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
4 changes: 2 additions & 2 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
run: pipx install poetry==2.3.2

- name: Set up Python 3.11
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.11"
cache: poetry
Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
run: pipx install poetry==2.3.2

- name: Set up Python 3.11
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.11"
cache: poetry
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# apollo

## 3.1.0

### Minor Changes

- f1df29c: global chat: add answer streaming to the planner, breaking up
responses into chunks which can be rendered earlier

### Patch Changes

- 6c322ae: global_chat: enable subagents to pull missing context, recovering
from routing errors

## 3.0.3

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "apollo",
"module": "platform/index.ts",
"version": "3.0.3",
"version": "3.1.0",
"type": "module",
"scripts": {
"start": "NODE_ENV=production bun platform/src/index.ts",
Expand Down
156 changes: 79 additions & 77 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ requires-poetry = ">=2.3.2"

[tool.poetry.dependencies]
python = "3.11.*"
openai = "^2.45"
openai = "^2.48"
python-dotenv = "^1.2.2"
anthropic = "^0.116.0"
anthropic = "^0.119.0"

langchain-pinecone = "^0.2.13"
langchain-core = "^1.4"
langchain-core = "^1.5"
langchain-community = "^0.4.2"
langchain-openai = "^1.3"
langchain-openai = "^1.4"
langchain-text-splitters = "^1.1"
nltk = "^3.10.0"
pytest = "^9.1.1"
sentry-sdk = "^2.64.0"
sentry-sdk = "^2.66.1"
psycopg2-binary = "^2.9.10"
langfuse = "^4.14.0"
langfuse = "^4.14.1"
opentelemetry-instrumentation-anthropic = "^0.62.1"
opentelemetry-instrumentation-threading = "0.64b0"
opentelemetry-instrumentation-threading = "0.65b0"

[tool.poetry.group.dev]
optional = false

[tool.poetry.group.dev.dependencies]
pytest = "^9.1.1"
ruff = "^0.15.21"
ruff = "^0.16.0"

[build-system]
requires = ["poetry-core"]
Expand Down
31 changes: 26 additions & 5 deletions services/global_chat/PAYLOAD_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ This document defines the input and output payload structure for the Global Agen

```json
{
"response": "string", // Main text response
"response": "string", // Main text response (final answer)

"response_segments": [ // Durable transcript of the turn, in stream order
{ "type": "text", "content": "I'll add the step..." },
{ "type": "status", "content": "Edited workflow structure" },
{ "type": "text", "content": "Done! I added..." }
],

"attachments": [ // Artifacts produced this turn
{
Expand All @@ -96,8 +102,8 @@ This document defines the input and output payload structure for the Global Agen
"history": [ // Conversation history including this turn
{
"role": "user|assistant",
"content": "string | array" // string for direct routes; array of content
} // blocks (text, tool_use, tool_result) for planner path
"content": "string"
}
],

"usage": { // Token usage (aggregated across all agents)
Expand Down Expand Up @@ -125,11 +131,26 @@ This document defines the input and output payload structure for the Global Agen

### Field Descriptions

- **`response`** (string): The main text response from the agent.
- **`response`** (string): The main text response from the agent — the final answer. On the planner path this is the text of the planner's last round only (narration from earlier rounds is not included).

- **`response_segments`** (array): The durable transcript of the turn in the order it was streamed, as `{"type", "content"}` objects. Two segment types:
- `text` — a text block from the model. On the planner path there is one per round of the tool-calling loop; the last one equals `response`.
- `status` — a completed-action status line ("Edited workflow structure", "Wrote code for \"Fetch Patients\""). Only these settled lines are recorded; the transient "...ing" spinners shown while an action runs are never persisted.

This lets the frontend persist and re-render the woven view after a page reload without reconstructing it from stream events. On direct routes (workflow_agent, job_code_agent) it is a single `text` segment wrapping `response` — statuses emitted internally by those subagents are not captured.

#### Streaming status events (planner path)

Apollo classifies status messages by event type so the client never has to infer their meaning from the text:

- **`thinking` events** (standard Anthropic thinking blocks) — transient progress spinners ("Reviewing the workflow...", "Writing code for \"X\"..."). Render live; each new status replaces the previous one; drop when the next text block starts. Never persist these.
- **`status` events** (custom event, like `changes`) — completed-action lines. Payload is `{"type": "status", "content": "Edited workflow structure"}`, identical to a `response_segments` entry, so live events and reloaded segments render through the same code path. Persist these (they resolve the preceding spinner).

Each tool beat streams as: `thinking` spinner → `changes` (if the workflow was modified) → `status` settled line → narration text.

- **`attachments`** (array): Artifacts produced during this turn. Each entry has a `type` and `content` field. An empty list `[]` means no artifacts were produced (e.g. a purely informational response). The only supported type is `workflow_yaml`: the full workflow YAML with any job code changes stitched in. Job code edits are never returned separately — the YAML is the single source of truth, which allows multi-step changes in one response.

- **`history`** (array): Updated conversation history including the latest exchange. On direct routes (workflow_agent, job_code_agent), each entry has `content` as a string. On the planner path, entries may have `content` as an array of content blocks (`text`, `tool_use`, `tool_result`) — this is the raw Anthropic messages format from the tool-calling loop.
- **`history`** (array): Updated conversation history including the latest exchange. Each entry has `content` as a string on every route. On the planner path the assistant entry contains only the final answer text — the pre-tool narration segments in `response` are not persisted to history.

- **`usage`** (object): Token usage aggregated across all agents invoked (router + planner + sub-agents).

Expand Down
24 changes: 18 additions & 6 deletions services/global_chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ Request to build a new multi-step workflow from scratch:
```json
{
"response": "I've created a workflow that fetches patient data from CommCare and loads it to DHIS2. The first job retrieves patient records via the CommCare REST API, and the second job maps and uploads them to DHIS2.",
"response_segments": [
{ "type": "text", "content": "I'll build the workflow structure first." },
{ "type": "status", "content": "Built workflow outline" },
{ "type": "status", "content": "Wrote code for \"Fetch from CommCare\", \"Load to DHIS2\"" },
{ "type": "text", "content": "I've created a workflow that fetches patient data from CommCare and loads it to DHIS2..." }
],
"attachments": [
{
"type": "workflow_yaml",
Expand Down Expand Up @@ -106,12 +112,18 @@ Request to build a new multi-step workflow from scratch:

**Response Fields:**

- `response`: The assistant's text response to the user
- `response`: The assistant's text response to the user — the final answer. On
the planner path this is the last round's text only; earlier narration lives
in `response_segments`
- `response_segments`: The durable transcript of the turn in stream order —
`text` segments woven with settled `status` lines ("Edited workflow
structure"). Transient "...ing" spinners are not included. See
`PAYLOAD_SPEC.md` for the full streaming event contract
- `attachments`: Artifacts produced — currently always
`{type: "workflow_yaml", content: string}` when YAML was generated or modified
- `history`: Updated conversation history including the latest exchange. Direct
routes return string `content`; the planner path may return `content` as
Anthropic content-block arrays (`tool_use`/`tool_result`)
- `history`: Updated conversation history including the latest exchange.
`content` is a string on every route; on the planner path the assistant entry
contains only the final answer text
- `usage`: Aggregated token usage across all agents called during the request
- `meta.agents`: Ordered list of agents invoked (e.g.
`["router", "workflow_agent"]` or
Expand Down Expand Up @@ -151,7 +163,7 @@ For straightforward requests, the router calls subagents directly:

### Planner

For complex requests, the `PlannerAgent` (Claude Sonnet) runs an agentic
For complex requests, the `PlannerAgent` (Claude Opus) runs an agentic
tool-calling loop with access to four tools:

- **`call_workflow_agent`** — create or modify workflow YAML structure
Expand All @@ -165,7 +177,7 @@ then calls `call_job_code_agent` for each job that needs code. Job code is
stitched into the workflow YAML immediately after each call.

The loop continues until the model signals it is done (up to a configurable
maximum of tool calls, default 25).
maximum of tool calls, currently 10 in `config.yaml`).

## Testing

Expand Down
1 change: 1 addition & 0 deletions services/global_chat/global_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def main(data_dict: dict) -> dict:
# 5. Return structured response
return {
"response": result.response,
"response_segments": result.response_segments,
"attachments": result.attachments,
"history": result.history,
"usage": result.usage,
Expand Down
Loading