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
30 changes: 15 additions & 15 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,25 @@ Run lint + type-check + tests before considering a change done.
`src/` layout, by subpackage:

- **`api/`** — FastAPI app (`app.py`), DI (`dependencies.py`), `schemas.py`,
and routes: `health`, `ingest`, `chat`, `title`, `vector_db`, plus
and routes: `health`, `ingest`, `buddy`, `title`, `vector_db`, plus
`onboarding` and `blueprints` (from #98).
- **`agents/`** — `ChatAgent` (`chat_agent.py`) runs one flat tool loop over a
single message list, capped by `_MAX_STEPS`: it searches while the model
asks for searches, then streams the answer from that same conversation.
Tool results carry the retrieved text, so the loop that searched is also the
loop that answers — there is no separate gather and answer phase.
`ChatOrchestrator` (`orchestrator.py`) maps the run's events into the SSE
stream consumed by `/api/v1/chat`. Tools live in `agents/tools/`, registered
via `ToolRegistry`.
- **`agents/`** — the buddy's tool-loop building blocks: tools live in
`agents/tools/` (retrieval, grep, evidence helpers), registered via
`ToolRegistry`, and `artifact_summary.py` backs the summaries endpoint. The
conversation engine itself is `onboarding/buddy_agent.py`: one flat tool
loop over a single message list, capped by `_MAX_STEPS` — it searches while
the model asks for searches, then answers from that same conversation. Tool
results carry the retrieved text, so the loop that searched is also the loop
that answers — there is no separate gather and answer phase.
- **`ingestion/`** — per-filetype parsers (`text_parser`, `pdf_parser`,
`code_parser`, `image_parser`) behind `parser.py`, then `chunker.py` and
`metadata_store.py`.
- **`rag/`** — `retriever.py`, `hybrid.py` (BM25 + vector hybrid retrieval),
`citation.py`, `prompt.py`.
- **`llm/`** — `LLMClient` interface (`base.py`), implemented by
`ollama_client.py`, `openai_client.py`, `anthropic_client.py`.
`split_client.py` lets chat and embeddings use different backends
(`LLM_BACKEND` for chat/generate/vision, `EMBED_BACKEND` for embeddings).
`split_client.py` lets generation and embeddings use different backends
(`LLM_BACKEND` for generate/chat/vision, `EMBED_BACKEND` for embeddings).
Every backend takes a request timeout (`LLM_TIMEOUT_SECONDS`, default 600).
The Anthropic client sets prompt-cache breakpoints on the system prompt and
the last message — see `anthropic_client._user_message` before changing how
Expand All @@ -79,15 +79,15 @@ Run lint + type-check + tests before considering a change done.
- Don't assume Ollama-only: check `llm/base.py`'s `LLMClient` interface when
touching anything that calls an LLM, since backend is configurable per
deployment (`LLM_BACKEND` / `EMBED_BACKEND`).
- Chat is one agent running one loop. Extend it with a new `Tool` in
- The buddy is one agent running one loop. Extend it with a new `Tool` in
`agents/tools/` rather than a sub-agent to delegate to: every tier costs a
serialized round-trip ahead of the user's first token, which is what the
previous orchestrator/synthesis split cost. Tool results must carry their
chunk text — a count-only summary forces a second pass to write the answer.
- **Tools must be read-only and thread-safe.** A turn's tool calls run
concurrently (`ChatAgent._run_tools`), so a tool that mutates shared state
would race. Anything cached across calls needs its own lock, the way
`rag.hybrid.BM25IndexCache` guards its rebuild.
concurrently (`agents.tools.base.run_tool_calls`), so a tool that mutates
shared state would race. Anything cached across calls needs its own lock,
the way `rag.hybrid.BM25IndexCache` guards its rebuild.
- `AGENT_DEBUG=1` logs each agent's reasoning (LLM text + tool calls) to
stderr — useful when debugging agent behavior.
- The onboarding pipeline is intentionally deterministic/staged rather than
Expand Down
34 changes: 19 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,22 @@ triggers Docker image publish to `ghcr.io/sprintstartproject/sprintstart-ai`.

- **`api/`** — FastAPI app (`app.py`), DI (`dependencies.py`), `schemas.py`,
SSE helpers (`sse.py`), and route modules under `routes/`.
- **`agents/`** — `ChatAgent` (`chat_agent.py`) runs one flat tool loop over a
single message list: it searches while the model asks, then streams the
answer from that same conversation. Tool results carry the retrieved text,
so the loop that searched is the loop that answers. Tools live in
`agents/tools/`, registered via `ToolRegistry`. `ChatOrchestrator`
(`orchestrator.py`) maps the run's events to SSE for `/api/v1/chat`.
- **`agents/`** — the buddy's tool-loop building blocks: tools live in
`agents/tools/` (retrieval, grep, evidence helpers), registered via
`ToolRegistry`, and `artifact_summary.py` backs the summaries endpoint. The
conversation engine itself is `onboarding/buddy_agent.py`
(`run_agent_turn`): one flat tool loop over a single message list — it
searches while the model asks, then answers from that same conversation.
Tool results carry the retrieved text, so the loop that searched is the
loop that answers. `api/routes/buddy.py` serves it.
- **`ingestion/`** — Per-filetype parsers (`text_parser`, `pdf_parser`,
`code_parser`, `image_parser`) behind `parser.py`, then `chunker.py` and
`metadata_store.py`.
- **`rag/`** — `retriever.py`, `hybrid.py` (BM25 + vector hybrid retrieval
with RRF fusion), `citation.py`, `prompt.py`.
- **`llm/`** — `LLMClient` protocol (`base.py`) with implementations:
`ollama_client.py`, `openai_client.py`, `anthropic_client.py`.
`SplitLLMClient` lets chat and embeddings use different backends
`SplitLLMClient` lets generation and embeddings use different backends
(`LLM_BACKEND` vs `EMBED_BACKEND`). All three take a request timeout
(`LLM_TIMEOUT_SECONDS`, default 600). The Anthropic client sets
prompt-cache breakpoints; caching matches on exact bytes, so read
Expand All @@ -76,11 +78,13 @@ scoping is **fail-closed** — a chunk with no project association is invisible
to all projects rather than visible to all of them. When touching retrieval,
ingest, or an endpoint, keep that property:

- `project_id` is required on `ChatRequest`, `OnboardingPathRequest`,
- `project_id` is required on `OnboardingPathRequest`,
`GenerateBlueprintsRequest`, `KnowledgeGapsRequest` and `FaqGroupRequest`
(all inherit `ProjectScopedRequest` in `api/schemas.py`).
`project_ids` (plural, required) is enforced on `AssembleOrientationRequest`,
`AssembleDiagramRequest`, and `MineStarterWorkRequest` for multi-project scoping.
`AssembleDiagramRequest`, and `MineStarterWorkRequest` for multi-project
scoping; `BuddyAgentRequest.project_ids` scopes the buddy's searches the
same way (empty admits nothing).
- `RetrievalFilters.project_id` / `project_ids` is enforced in **both** halves of hybrid
retrieval — `where_filter_for_chroma()` for the vector side and
`matches_retrieval_filters()` for the BM25/in-memory side (`rag/filters.py`).
Expand All @@ -102,13 +106,13 @@ ingest, or an endpoint, keep that property:
- Don't assume Ollama-only: check `llm/base.py`'s `LLMClient` protocol when
touching LLM calls. Backend is configurable per deployment via
`LLM_BACKEND` / `EMBED_BACKEND`.
- Chat is one agent, one loop. Give it a new `Tool` in `agents/tools/` rather
than a sub-agent to delegate to: every tier costs a serialized round-trip
ahead of the user's first token. Tool results must carry their chunk text —
a count-only summary forces a second pass to write the answer.
- The buddy is one agent, one loop. Give it a new `Tool` in `agents/tools/`
rather than a sub-agent to delegate to: every tier costs a serialized
round-trip ahead of the user's first token. Tool results must carry their
chunk text — a count-only summary forces a second pass to write the answer.
- Tools must be read-only and thread-safe: a turn's tool calls run
concurrently (`ChatAgent._run_tools`). Cache across calls only behind a lock,
as `rag.hybrid.BM25IndexCache` does.
concurrently (`agents.tools.base.run_tool_calls`). Cache across calls only
behind a lock, as `rag.hybrid.BM25IndexCache` does.
- `AGENT_DEBUG=1` logs each agent's reasoning (LLM text + tool calls) to
stderr — useful when debugging agent behavior.
- The onboarding pipeline is intentionally deterministic/staged rather than
Expand Down
4 changes: 2 additions & 2 deletions scripts/_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Shared HTTP client for the SprintStart AI terminal tools.

All the subcommands (chat, ingest, onboard, corpus) talk to the same running
All the subcommands (ingest, onboard, corpus) talk to the same running
service, so the connection handling, health check, SSE decoding, error
reporting, and document ingestion live here once. The offline chunk inspector
(`chunk_inspector_cli.py`) intentionally does not use this — it parses local
Expand Down Expand Up @@ -183,7 +183,7 @@ def detail(response: httpx.Response) -> str:
except (json.JSONDecodeError, ValueError):
return response.text

# --- ingestion (shared by the chat REPL and the ingest subcommand) ----
# --- ingestion (used by the ingest subcommand) -----------------------

def ingest_path(self, raw_path: str, artifact_id: str | None) -> None:
path = Path(raw_path).expanduser()
Expand Down
186 changes: 0 additions & 186 deletions scripts/chat_cli.py

This file was deleted.

5 changes: 1 addition & 4 deletions scripts/sprintstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
A single entrypoint over the running service with subcommands that share one
HTTP client (`_client.ServiceClient`):

sprintstart chat interactive Q&A over the corpus
sprintstart ingest <path> [id] ingest a file or directory
sprintstart onboard -a backend -e junior generate an onboarding path
sprintstart corpus show what's ingested (status + artifacts)
Expand All @@ -22,14 +21,12 @@

import argparse

import chat_cli
import corpus_cli
import ingest_cli
import onboarding_cli
from _client import add_base_url_arg

_COMMANDS = {
"chat": chat_cli,
"ingest": ingest_cli,
"onboard": onboarding_cli,
"corpus": corpus_cli,
Expand All @@ -45,7 +42,7 @@ def main() -> int:

for name, module in _COMMANDS.items():
sub = subparsers.add_parser(name, help=module.__doc__)
# --base-url goes after the subcommand: `sprintstart chat --base-url ...`.
# --base-url goes after the subcommand: `sprintstart ingest --base-url ...`.
add_base_url_arg(sub)
module.add_arguments(sub)
sub.set_defaults(func=module.run)
Expand Down
11 changes: 0 additions & 11 deletions src/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +0,0 @@
from agents.chat_agent import ChatAgent, ChatEvent, Evidence, Reasoning, Token
from agents.orchestrator import ChatOrchestrator

__all__ = [
"ChatAgent",
"ChatEvent",
"ChatOrchestrator",
"Evidence",
"Reasoning",
"Token",
]
4 changes: 2 additions & 2 deletions src/agents/artifact_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ def summarize_stream(
)
summary_parts: list[str] = []
for event in self._llm.stream(messages):
# Reasoning is live-only chat metadata. Summaries persist only the
# final user-visible text.
# Reasoning is live-only streaming metadata. Summaries persist
# only the final user-visible text.
if isinstance(event, TextDelta):
summary_parts.append(event.text)
yield event.text
Expand Down
Loading
Loading