Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .github/workflows/expert-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ on:
required: false
type: string
default: agent_schema.yaml
agent:
description: >-
Agent name (as declared in expert.toml or the name of a sibling
dir containing agent_schema.yaml). Omit for single-agent repos.
required: false
type: string
default: ""
suite:
description: 'Robot suite stem to run (e.g. "05_ask_latency"). Omit for all.'
required: false
Expand Down Expand Up @@ -102,6 +109,9 @@ jobs:
id: test
run: |
args=()
if [ -n "${{ inputs.agent }}" ]; then
args+=(--agent "${{ inputs.agent }}")
fi
if [ -n "${{ inputs.suite }}" ]; then
args+=(--suite "${{ inputs.suite }}")
fi
Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,59 @@ expert sync Push docs + rebuild Context Cache
expert ask "<question>" Stream answer from a deployed agent
expert sessions list/delete Manage user sessions (LGPD)
expert test Run the packaged E2E Robot Framework kit
expert agents List agents known to this workspace
expert use <name> Pin an agent as the active one for this workspace
expert which Print the agent a bare command would resolve to
```

Every command supports `--help` for full options.

### Multi-agent workspaces

Private repositories often host several specialists side-by-side. `expert`
auto-detects a multi-agent layout and lets you target any agent with three
equivalent syntaxes:

```bash
# One-off shortcut — great for quick hops between agents.
expert @ecg ask "What does lead V1 tell us?"
expert @derm ask "Differential for an acral nodule?"

# Explicit flag — CI-friendly, unambiguous.
expert ask --agent ecg "..."

# Pin for the current workspace (stored in .expert/state.json).
expert use ecg
expert ask "..." # now routes to ecg
expert use --clear # undo
```

Discovery order (first match wins):

1. `expert.toml` at the workspace root (authoritative — aliases, endpoints, defaults).
2. Any sibling directory containing `agent_schema.yaml` (auto-discovery).
3. Fallback: single-agent mode against `./agent_schema.yaml`.

An `expert.toml` looks like this:

```toml
[defaults]
agent = "ecg" # used when no flag / pin / env var is set

[agents.ecg]
schema = "ecg-expert/agent_schema.yaml"
endpoint = "https://ecg-agent.example.com"
api_key_env = "ECG_ADMIN_KEY" # reads from env at runtime
description = "12-lead ECG specialist"

[agents.derm]
schema = "derm-expert/agent_schema.yaml"
```

Prefix matching is supported: `expert @ec ask "..."` resolves to `ecg` if
no other agent name starts with `ec`. Ambiguous prefixes raise a friendly
error listing all candidates.

---

## End-to-end testing
Expand Down
131 changes: 131 additions & 0 deletions cli/expert/commands/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Workspace-aware agent management commands: ``agents``, ``use``, ``which``.

These are the only commands that *never* need to resolve to a single agent —
they inspect, select, or describe the workspace itself.
"""

from __future__ import annotations

from typing import Annotated

import typer
from rich.table import Table

from ..ui import console, print_error, print_info, print_success
from ..workspace import (
AgentNotFoundError,
AmbiguousAgentError,
Workspace,
WorkspaceError,
)


def agents_cmd(
verbose: Annotated[
bool,
typer.Option("--verbose", "-v", help="Show schema paths and endpoints."),
] = False,
) -> None:
"""List every agent known to this workspace."""
ws = Workspace.discover()
agents = ws.agents()
if not agents:
print_info(
"No agents found. Scaffold one with `expert init <name>` or create "
"an `expert.toml` workspace file."
)
return

active = ws.active()
table = Table(title=f"Agents — workspace: {ws.root}")
table.add_column("Active", width=6, justify="center")
table.add_column("Name", style="bold")
table.add_column("Source", style="dim")
if verbose:
table.add_column("Schema")
table.add_column("Endpoint")
table.add_column("Description", overflow="fold")

for info in agents:
is_active = "✓" if info.name == active else ""
row = [
is_active,
info.name,
info.source,
]
if verbose:
try:
schema_rel = str(info.schema_path.relative_to(ws.root))
except ValueError:
schema_rel = str(info.schema_path)
row.extend([schema_rel, info.endpoint or "—"])
row.append(info.description or "")
table.add_row(*row)

console.print(table)
if ws.default_agent:
print_info(f"default (expert.toml): [cyan]{ws.default_agent}[/cyan]")
if active:
print_info(f"active (expert use): [cyan]{active}[/cyan]")


def use_cmd(
name: Annotated[
str | None,
typer.Argument(
help="Agent name to pin as active. Omit to clear the pin.",
),
] = None,
clear: Annotated[
bool,
typer.Option("--clear", help="Remove the active-agent pointer."),
] = False,
) -> None:
"""Pin an agent as the active one for this workspace (stored locally)."""
ws = Workspace.discover()

if clear or (name is None):
if ws.state_file.is_file():
ws.clear_active()
print_success("Cleared active agent pointer.")
else:
print_info("No active agent set.")
return

try:
# Re-use matcher so `expert use derm` works when `derm-expert` is declared.
canonical = ws._match(name)
ws.set_active(canonical)
except (AgentNotFoundError, AmbiguousAgentError, WorkspaceError) as exc:
print_error(str(exc))
raise typer.Exit(code=1) from exc

print_success(f"Active agent set to [cyan]{canonical}[/cyan].")
print_info(f"State stored in {ws.state_file}")


def which_cmd(
agent: Annotated[
str | None,
typer.Option(
"--agent",
"-a",
help="Preview resolution for the given selector without running anything.",
),
] = None,
) -> None:
"""Print the agent a bare command (no --agent, no @alias) would resolve to."""
ws = Workspace.discover()
try:
ctx = ws.resolve(selector=agent)
except (AgentNotFoundError, AmbiguousAgentError, WorkspaceError) as exc:
print_error(str(exc))
raise typer.Exit(code=1) from exc

print_info(f"Active agent: [bold cyan]{ctx.name}[/bold cyan] (source: {ctx.selector_source})")
print_info(f" schema: {ctx.schema_path}")
print_info(f" endpoint: {ctx.endpoint or '—'}")
print_info(f" api key: {'set' if ctx.api_key else '—'}")


__all__ = ["agents_cmd", "use_cmd", "which_cmd"]
26 changes: 20 additions & 6 deletions cli/expert/commands/ask.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from rich.text import Text

from ..config import make_http_client
from ..context import resolve as resolve_context
from ..ui import console, print_error, print_info, print_success

_USER_ID = "cli"
Expand All @@ -56,22 +57,26 @@

def cmd(
question: Annotated[str, typer.Argument(help="Question to send to the agent.")],
agent: Annotated[
str | None,
typer.Option("--agent", "-a", help="Agent name from the workspace."),
] = None,
endpoint: Annotated[
str,
str | None,
typer.Option(
"--endpoint",
envvar="EXPERT_AGENT_ENDPOINT",
help="Base URL of the running agent.",
help="Override the agent's endpoint.",
),
],
] = None,
api_key: Annotated[
str,
str | None,
typer.Option(
"--api-key",
envvar="EXPERT_AGENT_API_KEY",
help="Admin bearer token.",
help="Override the agent's admin bearer token.",
),
],
] = None,
session: Annotated[
str | None,
typer.Option(
Expand All @@ -88,6 +93,15 @@ def cmd(
] = True,
) -> None:
"""Ask the agent a question."""
ctx = resolve_context(
agent=agent,
endpoint=endpoint,
api_key=api_key,
require_remote=True,
)
endpoint, api_key = ctx.require_remote()
if ctx.selector_source not in ("single", "schema-flag"):
print_info(f"→ [cyan]{ctx.name}[/cyan] ({ctx.selector_source})")
if session is None:
session = str(uuid.uuid4())
print_info(f"Starting new session [cyan]{session}[/cyan].")
Expand Down
20 changes: 16 additions & 4 deletions cli/expert/commands/count_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import typer
from app.schema import AgentSchema

from ..context import resolve as resolve_context
from ..ui import console, print_error, print_info, print_success, print_warning

if TYPE_CHECKING:
Expand Down Expand Up @@ -140,20 +141,31 @@ def cmd(
help="API key for google-genai token counting.",
),
],
agent: Annotated[
str | None,
typer.Option("--agent", "-a", help="Agent name from the workspace."),
] = None,
schema_path: Annotated[
Path,
typer.Option("--schema", "-s", help="Path to agent_schema.yaml."),
] = Path("./agent_schema.yaml"),
Path | None,
typer.Option(
"--schema",
"-s",
help="Explicit path to agent_schema.yaml (bypasses workspace resolution).",
),
] = None,
model: Annotated[
str,
typer.Option("--model", help="Model used for the count_tokens API call."),
] = "gemini-2.0-flash-exp",
) -> None:
"""Walk the knowledge base and sum the estimated token count per file."""
schema_path = schema_path.resolve()
ctx = resolve_context(agent=agent, schema=schema_path)
schema_path = ctx.schema_path
if not schema_path.is_file():
print_error(f"schema file not found: {schema_path}")
raise typer.Exit(code=1)
if ctx.selector_source not in ("single", "schema-flag"):
print_info(f"agent [cyan]{ctx.name}[/cyan] ({ctx.selector_source})")

try:
schema = AgentSchema.from_yaml(schema_path)
Expand Down
19 changes: 19 additions & 0 deletions cli/expert/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,24 @@ def cmd(
raise typer.Exit(code=1) from exc

print_success(f"Created new agent at [cyan]{path}[/cyan].")
_print_workspace_hint(path, name)
print_info("Next step: [bold]expert validate --schema ./agent_schema.yaml[/bold]")
console.print()


def _print_workspace_hint(path: Path, name: str) -> None:
"""If the new agent lives inside a multi-agent workspace, nudge the user."""
from ..workspace import Workspace

parent = path.parent
try:
ws = Workspace.discover(cwd=parent)
except Exception: # pragma: no cover - discovery is best-effort here
return

# Only hint when there's >1 agent (either discovered or declared).
if len(ws.agents_by_name) >= 2:
print_info(
f"Detected multi-agent workspace at [cyan]{ws.root}[/cyan]. "
f"Use [bold]expert agents[/bold] to list, or [bold]expert @{name} <cmd>[/bold]."
)
Loading
Loading