Skip to content

Commit c56494b

Browse files
authored
Merge branch 'main' into claude/funny-carson-duyxk8
2 parents ea7cc24 + 070c405 commit c56494b

11 files changed

Lines changed: 21 additions & 75 deletions

File tree

aai_cli/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ heavily-reworked commands with long bodies; small commands keep the inline
153153
- **`agent/`** — full-duplex voice agent (mic in, TTS out via `voices.py`).
154154
- **`agent_cascade/`** + `commands/agent_cascade/` — `assembly agent-cascade`: the same live terminal conversation as `assembly agent`, but **client-orchestrated** — `engine.run_cascade` wires Streaming STT → the LLM Gateway → streaming TTS itself instead of talking to the Voice Agent endpoint, mirroring what the `agent-cascade` `assembly init` template does server-side. **Sandbox-only** (streaming TTS has no prod host; guarded via `tts.session.require_available`). Reuses the agent slice's `DuplexAudio`/`AgentRenderer` and `core.client.stream_audio`/`core.llm.complete`/`tts.session.synthesize`; the three network legs are injected through `engine.CascadeDeps` (the `tts/session.py` seam) so the cascade — greeting, per-sentence TTS, barge-in, history window — is unit-tested against fakes with no sockets/mic/speaker. The LLM leg is a deepagents graph (`brain.py`); under `-v` (`debuglog.active()`) `brain._run_graph` *streams* that graph instead of `invoke`-ing it and logs each tool call/result/interim line as it lands (reusing `code_agent.events.message_events`), so a spoken turn that stalls mid-tool is debuggable — plain `invoke` runs the whole loop internally and `-v` would otherwise show only the httpx lines. **Front-end:** an interactive mic session in human mode runs a **voice-only Textual TUI** (`agent_cascade/tui.py`, `LiveAgentApp`) by default — there's no text input (you can't type to it), just a transcript + an animated voice bar tracking listening/thinking/speaking. It shares the `assembly code` TUI's chrome (`code_agent.banner` wordmark, `code_agent.messages` widgets, `code_agent.tui_status.voicebar_markup`/`VOICE_FRAMES`); the blocking `run_cascade` runs on a worker thread and reaches the UI through a `_TuiRenderer` (the `engine.Renderer` protocol) that hops each call onto the UI thread, and a quit calls `DuplexAudio.close` to end the mic iterator and unblock that worker. `_exec._should_use_tui` gates it: file/sample input, `--json`/`-o text`, and a non-TTY all fall back to the plain `AgentRenderer` line output.
155155
- **`tts/`** + `commands/speak.py` — `assembly speak` synthesizes text to speech over the sandbox streaming-TTS WebSocket (`streaming-tts.sandbox000.…`). **Sandbox-only:** `session.is_available()` is false in production (empty `Environment.streaming_tts_host`), so the command exits 2 with a `--sandbox` hint. `session.synthesize` drives a Begin→Generate→Flush→Audio→Terminate protocol with an injectable `connect` for hermetic tests (mirrors `agent/session.py`); `audio.py` plays the PCM (default) or writes a WAV (`--out`). The single-voice default-playback path **streams**: `synthesize`'s `on_audio(chunk, sample_rate)` callback is wired to `audio.PcmPlayer.feed`, so speech starts on the first Audio frame (it opens the device lazily, since the rate is only known at Begin) instead of after the whole text — the win for a long `--url` page. `--out` (needs the full buffer) and the multi-voice dialogue path (`synthesize_dialogue` → `_output_audio` → buffered `play_pcm`) stay buffered; `synthesize` still returns the complete PCM for the summary regardless.
156-
- **`code_agent/`** + `commands/code/` — `assembly code`: a terminal coding agent (a bespoke port of langchain-ai/deepagents' `code` agent) that talks **only** to the LLM Gateway. `model.py` pins the model to `ChatOpenAI` against `llm_gateway_base`; `agent.py` builds the deepagents graph over a cwd-scoped `LocalShellBackend` (filesystem + shell tools), plus extra tools: the custom `assembly` CLI tool (`cli_tool.py`, runs `python -m aai_cli` with the key via child env, never argv), a URL `fetch_url` tool (`fetch_tool.py`), Tavily web search when `TAVILY_API_KEY` is set (`web_search.py`), an `ask_user` tool routed through an `AskBridge` to the front-end (`ask_tool.py`), and best-effort docs MCP tools (`docs_mcp.py`). Middleware adds installed skills (`skills.py`) and long-term memory (`memory.py`), each over its own dedicated backend. Sessions persist via a SQLite checkpointer (`store.py`) keyed by `--session`, so conversations resume. Approval gates the mutating tools (write/edit/execute/`assembly`/`fetch_url`); the general-purpose `task` subagent comes from deepagents by default. `session.py` drives the graph turn-by-turn (interrupt/resume = human approval), emitting framework-agnostic `events.py` to either the Textual TUI (`tui.py`, modeled on deepagents-code: transcript + input + approval/ask modals + clipboard copy) or the Rich fallback (`render.py`). The whole orchestration is tested by driving the **real** graph with a fake `BaseChatModel` (`tests/test_code_agent.py`), so no network/TTY is needed. **Voice is the default front-end in an interactive TTY** (`voice.py` + `_exec._run_voice`): `VoiceSession.listen` captures one spoken turn over Streaming STT (gating the mic shut the instant a turn finalizes) and `VoiceSession.speak` reads each assistant reply back over streaming TTS. It runs the **Rich REPL** loop (not the keyboard TUI) with a voice `read_line` + a reply-speaking sink. Readback needs streaming TTS, so it's **sandbox-only** (`tts.session.is_available`); in production the mic input still works and replies stay on screen. A mic-less box degrades to typed input on the first `AUDIO_ERROR_TYPES` `CLIError`; `--no-voice` selects the TUI, and a non-TTY (pipe/CI) the headless loop. Both legs (STT/TTS) are injected like the cascade's, so `tests/test_code_voice.py` drives it with fakes — no mic/speaker/socket.
156+
- **`code_agent/`** + `commands/code/` — `assembly code`: a terminal coding agent (a bespoke port of langchain-ai/deepagents' `code` agent) that talks **only** to the LLM Gateway. `model.py` pins the model to `ChatOpenAI` against `llm_gateway_base`; `agent.py` builds the deepagents graph over a cwd-scoped `LocalShellBackend` (filesystem + shell tools), plus extra tools: the custom `assembly` CLI tool (`cli_tool.py`, runs `python -m aai_cli` with the key via child env, never argv), a URL `fetch_url` tool (`fetch_tool.py`), Firecrawl web search when `FIRECRAWL_API_KEY` is set (`firecrawl_search.py`, shared with the live voice agent), an `ask_user` tool routed through an `AskBridge` to the front-end (`ask_tool.py`), and best-effort docs MCP tools (`docs_mcp.py`). Middleware adds installed skills (`skills.py`) and long-term memory (`memory.py`), each over its own dedicated backend. Sessions persist via a SQLite checkpointer (`store.py`) keyed by `--session`, so conversations resume. Approval gates the mutating tools (write/edit/execute/`assembly`/`fetch_url`); the general-purpose `task` subagent comes from deepagents by default. `session.py` drives the graph turn-by-turn (interrupt/resume = human approval), emitting framework-agnostic `events.py` to either the Textual TUI (`tui.py`, modeled on deepagents-code: transcript + input + approval/ask modals + clipboard copy) or the Rich fallback (`render.py`). The whole orchestration is tested by driving the **real** graph with a fake `BaseChatModel` (`tests/test_code_agent.py`), so no network/TTY is needed. **Voice is the default front-end in an interactive TTY** (`voice.py` + `_exec._run_voice`): `VoiceSession.listen` captures one spoken turn over Streaming STT (gating the mic shut the instant a turn finalizes) and `VoiceSession.speak` reads each assistant reply back over streaming TTS. It runs the **Rich REPL** loop (not the keyboard TUI) with a voice `read_line` + a reply-speaking sink. Readback needs streaming TTS, so it's **sandbox-only** (`tts.session.is_available`); in production the mic input still works and replies stay on screen. A mic-less box degrades to typed input on the first `AUDIO_ERROR_TYPES` `CLIError`; `--no-voice` selects the TUI, and a non-TTY (pipe/CI) the headless loop. Both legs (STT/TTS) are injected like the cascade's, so `tests/test_code_voice.py` drives it with fakes — no mic/speaker/socket.
157157
- **`code_gen/`** — backs `--show-code` on `transcribe`/`stream`/`agent`: builds a ready-to-run Python SDK script from exactly the flags passed (no API key needed; generated code reads `ASSEMBLYAI_API_KEY`).
158158
- **`auth/`** — browser-assisted `assembly login` via AMS + **Stytch B2B OAuth discovery** (`discovery.py`, `flow.py`, `loopback.py`, `ams.py`). Not Stytch Connected Apps.
159159
- **`init/`** — scaffolds a self-contained FastAPI + HTML starter (`audio-transcription`/`live-captions`/`voice-agent` templates), optionally installs deps and opens the browser; writes the key to a git-ignored `.env`.

aai_cli/code_agent/fetch_tool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""A URL-fetch tool for the coding agent (deepagents-code parity).
22
3-
Distinct from web *search* (Tavily): this fetches a specific URL the agent already
3+
Distinct from web *search* (Firecrawl): this fetches a specific URL the agent already
44
knows and returns its text. It is approval-gated (see ``MUTATING_TOOLS``) because an
55
arbitrary fetch can reach internal/SSRF targets, so the user confirms each one.
66
"""

aai_cli/code_agent/firecrawl_search.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
"""Optional Firecrawl web search for the live voice agent.
1+
"""Optional Firecrawl web search for the coding and live voice agents.
22
33
Firecrawl grounds the agent with live web search, enabled when a ``FIRECRAWL_API_KEY``
44
is present in the environment. Search is read-only, so it is *not* gated behind the
55
approval flow. With no key set we simply omit the tool (the agent still has its URL
66
fetch and the AssemblyAI docs MCP), rather than erroring.
77
8-
This mirrors ``web_search.py`` (Tavily) but reuses Firecrawl's official LangChain
9-
integration; the live agent prefers it as its default search tool.
8+
Both ``assembly code`` (approval-gated, opt-out via ``--no-web``) and the live voice
9+
agent share this single search tool via Firecrawl's official LangChain integration.
1010
"""
1111

1212
from __future__ import annotations

aai_cli/code_agent/web_search.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

aai_cli/commands/code/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def code(
5858
True, "--skills/--no-skills", help="Load installed agent skills (e.g. the assemblyai skill)"
5959
),
6060
web: bool = typer.Option(
61-
True, "--web/--no-web", help="Enable Tavily web search when TAVILY_API_KEY is set"
61+
True, "--web/--no-web", help="Enable Firecrawl web search when FIRECRAWL_API_KEY is set"
6262
),
6363
memory: bool = typer.Option(
6464
True, "--memory/--no-memory", help="Load and persist the agent's long-term memory"

aai_cli/commands/code/_exec.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from aai_cli.code_agent.docs_mcp import load_docs_tools
2626
from aai_cli.code_agent.events import AssistantText, Event
2727
from aai_cli.code_agent.fetch_tool import build_fetch_tool
28+
from aai_cli.code_agent.firecrawl_search import FIRECRAWL_API_KEY_ENV, build_web_search_tool
2829
from aai_cli.code_agent.memory import build_memory_middleware
2930
from aai_cli.code_agent.model import build_model
3031
from aai_cli.code_agent.prompt import DEFAULT_MODEL
@@ -38,7 +39,6 @@
3839
build_voice_session,
3940
spoken_summary,
4041
)
41-
from aai_cli.code_agent.web_search import TAVILY_API_KEY_ENV, build_web_search_tool
4242
from aai_cli.core import env, errors, stdio
4343
from aai_cli.ui import output
4444

@@ -136,10 +136,11 @@ def _read_line() -> str | None:
136136

137137

138138
def _web_note(opts: CodeOptions) -> str | None:
139-
"""The "web search disabled" notice when --web is on but no Tavily key is set."""
140-
if opts.web and not env.get(TAVILY_API_KEY_ENV):
139+
"""The "web search disabled" notice when --web is on but no Firecrawl key is set."""
140+
if opts.web and not env.get(FIRECRAWL_API_KEY_ENV):
141141
return (
142-
"TAVILY_API_KEY is not set, so web search is disabled. Get a key at https://tavily.com"
142+
"FIRECRAWL_API_KEY is not set, so web search is disabled. "
143+
"Get a key at https://firecrawl.dev"
143144
)
144145
return None
145146

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ dependencies = [
8080
"langchain-core>=1.4.7",
8181
"langchain-mcp-adapters>=0.3.0",
8282
"textual>=8.2.7",
83-
"langchain-tavily>=0.2.18",
8483
"langgraph-checkpoint-sqlite>=3.1.0",
8584
"pyperclip>=1.11.0",
8685
"langchain-text-splitters>=1.0.0",

tests/__snapshots__/test_snapshots_help_run.ambr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,8 @@
299299
│ --skills --no-skills Load installed agent skills (e.g. │
300300
│ the assemblyai skill) │
301301
│ [default: skills] │
302-
│ --web --no-web Enable Tavily web search when
303-
TAVILY_API_KEY is set
302+
│ --web --no-web Enable Firecrawl web search when │
303+
FIRECRAWL_API_KEY is set │
304304
│ [default: web] │
305305
│ --memory --no-memory Load and persist the agent's │
306306
│ long-term memory │

tests/test_code_agent.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
docs_mcp,
2121
events,
2222
fetch_tool,
23+
firecrawl_search,
2324
memory,
2425
skills,
2526
store,
26-
web_search,
2727
)
2828
from aai_cli.code_agent import model as model_mod
2929
from aai_cli.code_agent.agent import MUTATING_TOOLS, build_agent
@@ -225,12 +225,12 @@ def test_skills_middleware_present_and_absent(tmp_path: Path) -> None:
225225

226226

227227
def test_web_search_tool_gated_on_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
228-
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
229-
assert web_search.build_web_search_tool() is None
228+
monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False)
229+
assert firecrawl_search.build_web_search_tool() is None
230230

231-
monkeypatch.setenv("TAVILY_API_KEY", "tvly-key")
232-
tool = web_search.build_web_search_tool()
233-
assert tool is not None and tool.name == "tavily_search"
231+
monkeypatch.setenv("FIRECRAWL_API_KEY", "fc-key")
232+
tool = firecrawl_search.build_web_search_tool()
233+
assert tool is not None and tool.name == "firecrawl_search"
234234

235235

236236
def test_message_events_coerces_list_content() -> None:

tests/test_code_command.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,10 @@ def test_build_agent_wires_model_tools_and_checkpointer(monkeypatch):
192192

193193

194194
def test_web_note_only_without_key(monkeypatch):
195-
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
195+
monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False)
196196
assert _exec._web_note(_opts(web=True)) is not None
197197
assert _exec._web_note(_opts(web=False)) is None
198-
monkeypatch.setenv("TAVILY_API_KEY", "tvly")
198+
monkeypatch.setenv("FIRECRAWL_API_KEY", "fc-x")
199199
assert _exec._web_note(_opts(web=True)) is None
200200

201201

0 commit comments

Comments
 (0)