Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/setup/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ fastagent.jsonl
# Editors (optional)
.idea/
.vscode/

# Packaging metadata
*.dist-info/
*.egg-info/
dist/
build/
9 changes: 8 additions & 1 deletion examples/setup/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@
fast = FastAgent("fast-agent example")


default_instruction = """You are a helpful AI Agent.

{{serverInstructions}}

The current date is {{currentDate}}."""


# Define the agent
@fast.agent(instruction="You are a helpful AI Agent")
@fast.agent(instruction=default_instruction)
async def main():
# use the --model command line switch or agent arguments to change model
async with fast.run() as agent:
Expand Down
6 changes: 6 additions & 0 deletions examples/setup/pyproject.toml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ package = true
[project.scripts]
fast-agent-app = "agent:main"

[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
py-modules = ["agent"]
16 changes: 15 additions & 1 deletion src/fast_agent/agents/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,24 @@ async def show_assistant_message(
display_name = name if name is not None else self.name
display_model = model if model is not None else (self.llm.model_name if self._llm else None)

# Convert highlight_items to highlight_index
highlight_index = None
if highlight_items and bottom_items:
if isinstance(highlight_items, str):
try:
highlight_index = bottom_items.index(highlight_items)
except ValueError:
pass
elif isinstance(highlight_items, list) and len(highlight_items) > 0:
try:
highlight_index = bottom_items.index(highlight_items[0])
except ValueError:
pass

await self.display.show_assistant_message(
message_text,
bottom_items=bottom_items,
highlight_items=highlight_items,
highlight_index=highlight_index,
max_item_length=max_item_length,
name=display_name,
model=display_model,
Expand Down
74 changes: 73 additions & 1 deletion src/fast_agent/agents/mcp_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ async def initialize(self) -> None:
"""
await self.__aenter__()

# Apply template substitution to the instruction with server instructions
await self._apply_instruction_templates()

async def shutdown(self) -> None:
"""
Shutdown the agent and close all MCP server connections.
Expand All @@ -174,6 +177,67 @@ def initialized(self, value: bool) -> None:
self._initialized = value
self._aggregator.initialized = value

async def _apply_instruction_templates(self) -> None:
"""
Apply template substitution to the instruction, including server instructions.
This is called during initialization after servers are connected.
"""
if not self.instruction:
return

# Gather server instructions if the template includes {{serverInstructions}}
if "{{serverInstructions}}" in self.instruction:
try:
instructions_data = await self._aggregator.get_server_instructions()
server_instructions = self._format_server_instructions(instructions_data)
except Exception as e:
self.logger.warning(f"Failed to get server instructions: {e}")
server_instructions = ""

# Replace the template variable
self.instruction = self.instruction.replace("{{serverInstructions}}", server_instructions)


# Update default request params to match
if self._default_request_params:
self._default_request_params.systemPrompt = self.instruction

self.logger.debug(f"Applied instruction templates for agent {self._name}")

def _format_server_instructions(self, instructions_data: Dict[str, tuple[str | None, List[str]]]) -> str:
"""
Format server instructions with XML tags and tool lists.

Args:
instructions_data: Dict mapping server name to (instructions, tool_names)

Returns:
Formatted string with server instructions
"""
if not instructions_data:
return ""

formatted_parts = []
for server_name, (instructions, tool_names) in instructions_data.items():
# Skip servers with no instructions
if instructions is None:
continue

# Format tool names with server prefix
prefixed_tools = [f"{server_name}-{tool}" for tool in tool_names]
tools_list = ", ".join(prefixed_tools) if prefixed_tools else "No tools available"

formatted_parts.append(
f"<mcp-server name=\"{server_name}\">\n"
f"<tools>{tools_list}</tools>\n"
f"<instructions>\n{instructions}\n</instructions>\n"
f"</mcp-server>"
)

if formatted_parts:
return "\n\n".join(formatted_parts)
return ""

async def __call__(
self,
message: Union[
Expand Down Expand Up @@ -549,12 +613,20 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
namespaced_tool = self._aggregator._namespaced_tool_map.get(tool_name)
display_tool_name = namespaced_tool.tool.name if namespaced_tool else tool_name

# Find the index of the current tool in available_tools for highlighting
highlight_index = None
try:
highlight_index = available_tools.index(display_tool_name)
except ValueError:
# Tool not found in list, no highlighting
pass

self.display.show_tool_call(
name=self._name,
tool_args=tool_args,
bottom_items=available_tools,
tool_name=display_tool_name,
highlight_items=tool_name,
highlight_index=highlight_index,
max_item_length=12,
)

Expand Down
10 changes: 10 additions & 0 deletions src/fast_agent/agents/tool_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,21 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
for correlation_id, tool_request in request.tool_calls.items():
tool_name = tool_request.params.name
tool_args = tool_request.params.arguments or {}

# Find the index of the current tool in available_tools for highlighting
highlight_index = None
try:
highlight_index = available_tools.index(tool_name)
except ValueError:
# Tool not found in list, no highlighting
pass

self.display.show_tool_call(
name=self.name,
tool_args=tool_args,
bottom_items=available_tools,
tool_name=tool_name,
highlight_index=highlight_index,
max_item_length=12,
)

Expand Down
12 changes: 10 additions & 2 deletions src/fast_agent/agents/workflow/router_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,18 @@ async def _route_request(
if response.reasoning:
routing_message += f" ({response.reasoning})"

# Convert highlight_items to highlight_index
agent_keys = list(self.agent_map.keys())
highlight_index = None
try:
highlight_index = agent_keys.index(response.agent)
except ValueError:
pass

await self.display.show_assistant_message(
routing_message,
bottom_items=list(self.agent_map.keys()),
highlight_items=[response.agent],
bottom_items=agent_keys,
highlight_index=highlight_index,
name=self.name,
)

Expand Down
81 changes: 52 additions & 29 deletions src/fast_agent/cli/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,28 @@
app = typer.Typer(help="Manage OAuth authentication state for MCP servers")


def _get_keyring_backend_name() -> str:
def _get_keyring_status() -> tuple[str, bool]:
"""Return (backend_name, usable) where usable=False for the fail backend or missing keyring."""
try:
import keyring

kr = keyring.get_keyring()
return getattr(kr, "name", kr.__class__.__name__)
name = getattr(kr, "name", kr.__class__.__name__)
try:
from keyring.backends.fail import Keyring as FailKeyring # type: ignore

return name, not isinstance(kr, FailKeyring)
except Exception:
# If fail backend marker cannot be imported, assume usable
return name, True
except Exception:
return "unavailable"
return "unavailable", False


def _get_keyring_backend_name() -> str:
# Backwards-compat helper; prefer _get_keyring_status in new code
name, _ = _get_keyring_status()
return name


def _keyring_get_password(service: str, username: str) -> str | None:
Expand Down Expand Up @@ -106,7 +120,7 @@ def status(
) -> None:
"""Show keyring backend and token status for configured MCP servers."""
settings = get_settings(config_path)
backend = _get_keyring_backend_name()
backend, backend_usable = _get_keyring_status()

# Single-target view if target provided
if target:
Expand All @@ -123,12 +137,15 @@ def status(

# Direct presence check
present = False
try:
import keyring
if backend_usable:
try:
import keyring

present = keyring.get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
except Exception:
present = False
present = (
keyring.get_password("fast-agent-mcp", f"oauth:tokens:{identity}") is not None
)
except Exception:
present = False

table = Table(show_header=True, box=None)
table.add_column("Identity", header_style="bold")
Expand All @@ -139,7 +156,10 @@ def status(
token_disp = "[bold green]✓[/bold green]" if present else "[dim]✗[/dim]"
table.add_row(identity, token_disp, servers_for_id)

console.print(f"Keyring backend: [green]{backend}[/green]")
if backend_usable and backend != "unavailable":
console.print(f"Keyring backend: [green]{backend}[/green]")
else:
console.print("Keyring backend: [red]not available[/red]")
console.print(table)
console.print(
"\n[dim]Run 'fast-agent auth clear --identity "
Expand All @@ -148,7 +168,10 @@ def status(
return

# Full status view
console.print(f"Keyring backend: [green]{backend}[/green]")
if backend_usable and backend != "unavailable":
console.print(f"Keyring backend: [green]{backend}[/green]")
else:
console.print("Keyring backend: [red]not available[/red]")

tokens = list_keyring_tokens()
token_table = Table(show_header=True, box=None)
Expand Down Expand Up @@ -181,25 +204,25 @@ def status(
)
# Direct presence check for each identity so status works even without index
has_token = False
token_disp = "[dim]✗[/dim]"
if persist == "keyring" and row["oauth"]:
try:
import keyring

has_token = (
keyring.get_password("fast-agent-mcp", f"oauth:tokens:{row['identity']}")
is not None
)
except Exception:
has_token = False
token_disp = (
"[bold green]✓[/bold green]"
if has_token
else (
"[yellow]memory[/yellow]"
if persist == "memory" and row["oauth"]
else "[dim]✗[/dim]"
)
)
if backend_usable:
try:
import keyring

has_token = (
keyring.get_password(
"fast-agent-mcp", f"oauth:tokens:{row['identity']}"
)
is not None
)
except Exception:
has_token = False
token_disp = "[bold green]✓[/bold green]" if has_token else "[dim]✗[/dim]"
else:
token_disp = "[red]not available[/red]"
elif persist == "memory" and row["oauth"]:
token_disp = "[yellow]memory[/yellow]"
map_table.add_row(
row["name"],
row["transport"].upper(),
Expand Down
Loading
Loading