diff --git a/.env.example b/.env.example index d6df131..1ab5112 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,8 @@ POSTGRES_DB=synthora # --- LLM providers --- OPENAI_API_KEY= OPENAI_BASE_URL= # optional: OpenRouter / vLLM / LM Studio +# When using docker compose --profile ollama with docker-compose.ollama.yml: +# OLLAMA_BASE_URL=http://ollama:11434 OLLAMA_BASE_URL=http://localhost:11434 # --- Search --- diff --git a/README.md b/README.md index ad8138a..77429a8 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,13 @@ LangGraph Studio: `uv run langgraph dev` (uses `langgraph.json`). docker compose up -d ``` -Brings up: API (`:8000`), worker, web UI (`:3000`), Postgres, Redis, SearXNG. Optional local LLM: `docker compose --profile ollama up -d`. +Brings up: API (`:8000`), worker, web UI (`:3000`), Postgres, Redis, SearXNG. Optional local LLM: + +```bash +docker compose --profile ollama -f docker-compose.yml -f docker-compose.ollama.yml up -d +``` + +The overlay sets `OLLAMA_BASE_URL=http://ollama:11434` on API/worker. Default embeddings use `SYNTHORA_EMBEDDINGS=hash` (no Ollama required). Key environment variables (see `.env.example`): `SYNTHORA_DATABASE_URL`, `SYNTHORA_REDIS_URL`, `SYNTHORA_AUTH_MODE` (`none`|`session`), `OPENAI_API_KEY` / `OPENAI_BASE_URL`, `TAVILY_API_KEY`, `SEARXNG_URL`. diff --git a/docker-compose.ollama.yml b/docker-compose.ollama.yml new file mode 100644 index 0000000..725450c --- /dev/null +++ b/docker-compose.ollama.yml @@ -0,0 +1,10 @@ +# Optional overlay when starting with `--profile ollama`. +# Usage: +# docker compose --profile ollama -f docker-compose.yml -f docker-compose.ollama.yml up -d +services: + api: + environment: + OLLAMA_BASE_URL: http://ollama:11434 + worker: + environment: + OLLAMA_BASE_URL: http://ollama:11434 diff --git a/docs/feature-parity.md b/docs/feature-parity.md index 348f253..914cc8d 100644 --- a/docs/feature-parity.md +++ b/docs/feature-parity.md @@ -140,6 +140,11 @@ Closed on ``feat/compose-embeddings-default``: compose defaults when profile disabled); sync ``SynthoraClient.iter_run_events``; embedding default tests; parity audit doc sync. +Closed on ``feat/ollama-profile-and-async-ws``: ``docker-compose.ollama.yml`` +overlay wires ``OLLAMA_BASE_URL`` for the ollama profile; async +``iter_run_events`` handles ``ConnectionClosed`` like sync; README/smoke +comments updated. + No known functional gaps remain beyond explicit non-goals below. Chat remains session-scoped ``fast_research`` with prior-report memory — diff --git a/docs/parity-audit.md b/docs/parity-audit.md index 358913c..b51a8c3 100644 --- a/docs/parity-audit.md +++ b/docs/parity-audit.md @@ -1,7 +1,7 @@ # Synthora parity audit checklist Living checklist against Open Deep Research, STORM/Co-STORM, and Local Deep -Research. Last verified on `main` after PR #12 (`2537bd6`). +Research. Last verified on `main` after PR #13 (`8e13595`) and ollama-profile overlay. ## Open Deep Research @@ -69,6 +69,7 @@ Research. Last verified on `main` after PR #12 (`2537bd6`). | Item | Status | |---|---| | Compose default embeddings (`SYNTHORA_EMBEDDINGS=hash`) without Ollama profile | done (PR #13) | +| Ollama profile overlay (`docker-compose.ollama.yml` sets service URL) | done (PR #14) | | Live compose smoke (`scripts/smoke.sh`: research, export, upload) | done | | Playwright UI e2e | API-mocked (CI speed); not a live-stack browser gate | diff --git a/packages/sdk/src/synthora/sdk/async_client.py b/packages/sdk/src/synthora/sdk/async_client.py index 3044d9f..b569f2e 100644 --- a/packages/sdk/src/synthora/sdk/async_client.py +++ b/packages/sdk/src/synthora/sdk/async_client.py @@ -159,6 +159,7 @@ async def wait_for_report( async def iter_run_events(self, run_id: str) -> AsyncIterator[dict]: """Stream live events from the run WebSocket.""" import websockets + from websockets.exceptions import ConnectionClosed url = self.events_ws_url(run_id) headers = [] @@ -166,7 +167,10 @@ async def iter_run_events(self, run_id: str) -> AsyncIterator[dict]: headers.append(("Authorization", f"Bearer {self.token}")) async with websockets.connect(url, additional_headers=headers) as ws: while True: - raw = await ws.recv() + try: + raw = await ws.recv() + except ConnectionClosed: + break yield json.loads(raw) async def chat(self, message: str, *, session_id: Optional[str] = None) -> dict: diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 1f86b19..63239c7 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -4,8 +4,7 @@ set -euo pipefail cd "$(dirname "$0")/.." -# Compose defaults OLLAMA_BASE_URL to the optional ollama profile service; smoke -# uses deterministic hash embeddings unless callers override these explicitly. +# Compose defaults hash embeddings (no Ollama required). Override OPENAI/OLLAMA as needed. export OPENAI_API_KEY="${OPENAI_API_KEY:-}" export OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-}" export SYNTHORA_EMBEDDINGS="${SYNTHORA_EMBEDDINGS:-hash}" diff --git a/tests/test_sdk.py b/tests/test_sdk.py index 4e094b9..835192e 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -209,6 +209,41 @@ def recv(self): assert events[-1]["type"] == "done" +@pytest.mark.asyncio +async def test_async_iter_run_events(monkeypatch): + from synthora.sdk.async_client import AsyncSynthoraClient + from websockets.exceptions import ConnectionClosedOK + + payloads = [ + {"type": "status", "message": "queued"}, + {"type": "done", "message": "completed"}, + ] + + class _FakeWS: + def __init__(self): + self._queue = list(payloads) + + async def __aenter__(self): + return self + + async def __aexit__(self, *_args): + return False + + async def recv(self): + if self._queue: + return json.dumps(self._queue.pop(0)) + raise ConnectionClosedOK(None, None) + + monkeypatch.setattr( + "websockets.connect", + lambda *_a, **_k: _FakeWS(), + ) + client = AsyncSynthoraClient("http://localhost:8000") + events = [event async for event in client.iter_run_events("run-1")] + assert events[0]["type"] == "status" + assert events[-1]["type"] == "done" + + @pytest.mark.asyncio async def test_async_sdk_health_and_mcp(platform): from httpx import ASGITransport