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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ SYNTHORA_MCP_ALLOWED_ORIGINS= # e.g. https://app.example.com
SYNTHORA_NEWS_POLL=1 # worker background news poller
SYNTHORA_NEWS_POLL_INTERVAL=300
SYNTHORA_USE_ALEMBIC=1 # run alembic upgrade head on postgres boot
SYNTHORA_EMBEDDINGS=hash # hash | openai | ollama (compose default: hash)
SYNTHORA_API_PORT=8000
SYNTHORA_WEB_PORT=3000

Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-}
TAVILY_API_KEY: ${TAVILY_API_KEY:-}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
SYNTHORA_EMBEDDINGS: ${SYNTHORA_EMBEDDINGS:-}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-}
SYNTHORA_EMBEDDINGS: ${SYNTHORA_EMBEDDINGS:-hash}
ports:
- "${SYNTHORA_API_PORT:-8000}:8000"
depends_on:
Expand Down
5 changes: 5 additions & 0 deletions docs/feature-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ integration tests; ``AsyncSynthoraClient`` with ``iter_run_events`` WebSocket;
News subscription filter + ``getNewsSubscription`` UX; smoke validates export
formats and document upload.

Closed on ``feat/compose-embeddings-default``: compose defaults
``SYNTHORA_EMBEDDINGS=hash`` and empty ``OLLAMA_BASE_URL`` (no phantom Ollama
when profile disabled); sync ``SynthoraClient.iter_run_events``; embedding
default tests; parity audit doc sync.

No known functional gaps remain beyond explicit non-goals below.

Chat remains session-scoped ``fast_research`` with prior-report memory —
Expand Down
18 changes: 14 additions & 4 deletions docs/parity-audit.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Synthora parity audit checklist

Living checklist against Open Deep Research, STORM/Co-STORM, and Local Deep
Research. Updated on `feat/synthora-full-parity`.
Research. Last verified on `main` after PR #12 (`2537bd6`).

## Open Deep Research

Expand Down Expand Up @@ -46,10 +46,11 @@ Research. Updated on `feat/synthora-full-parity`.
| Settings persistence | done |
| Export md/html/pdf | done |
| Delete / clear history | done |
| MCP server (outbound tools API) | done |
| MCP server (inbound agent surface + outbound researcher tools) | done |
| News / subscriptions | done |
| Metrics | done |
| Chat / follow-up research | done |
| Python SDK (sync + async + WebSocket iterators) | done |

## Security & isolation (verified by `tests/test_isolation.py`)

Expand All @@ -63,14 +64,23 @@ Research. Updated on `feat/synthora-full-parity`.
| Boot refusal on insecure secret key in session mode | done |
| Durable Postgres checkpointer (compose default `postgres`) | done |

## Deployment defaults

| Item | Status |
|---|---|
| Compose default embeddings (`SYNTHORA_EMBEDDINGS=hash`) without Ollama profile | done (PR #13) |
| Live compose smoke (`scripts/smoke.sh`: research, export, upload) | done |
| Playwright UI e2e | API-mocked (CI speed); not a live-stack browser gate |

## Residual gaps (deliberate, not silent)

None remaining for in-scope functionality. Compose smoke and Playwright
API-mock e2e are verified.
No in-scope product/API/worker gaps. Inbound MCP exposes four agent tools
(not the full REST catalog) by design. Chat enqueues `fast_research` by design.

## Explicit non-goals

- Vendoring upstream source trees as disconnected apps
- Per-user SQLCipher (shared Postgres + optional auth by design)
- STORM Streamlit demo
- Paper eval dataset construction pipelines
- Full CommonMark/GFM export (Synthora report subset only)
20 changes: 20 additions & 0 deletions packages/sdk/src/synthora/sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import time
from collections.abc import Iterator
from typing import Any, Optional

import httpx
Expand Down Expand Up @@ -121,6 +122,25 @@ def events_ws_url(self, run_id: str) -> str:
suffix = f"?token={self.token}" if self.token else ""
return f"{ws_base}/api/v1/research/{run_id}/events/ws{suffix}"

def iter_run_events(self, run_id: str) -> Iterator[dict]:
"""Yield live run events over WebSocket (requires a reachable server)."""
import json

from websockets.exceptions import ConnectionClosed
from websockets.sync.client import connect

url = self.events_ws_url(run_id)
headers: list[tuple[str, str]] = []
if self.token:
headers.append(("Authorization", f"Bearer {self.token}"))
with connect(url, additional_headers=headers) as ws:
while True:
try:
raw = ws.recv()
except ConnectionClosed:
break
yield json.loads(raw)

def download_export(self, run_id: str, fmt: str = "markdown") -> bytes:
"""Download export bytes with auth (session mode safe)."""
resp = self._client.get(
Expand Down
38 changes: 38 additions & 0 deletions tests/test_embeddings_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Default embedding resolution for compose and offline dev."""

from __future__ import annotations

from synthora.adapters.embeddings import (
HashEmbeddings,
OllamaEmbeddings,
OpenAIEmbeddings,
resolve_default_embeddings,
)


def test_resolve_default_embeddings_uses_hash_without_keys(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
monkeypatch.delenv("OLLAMA_EMBED_MODEL", raising=False)
monkeypatch.delenv("SYNTHORA_EMBEDDINGS", raising=False)
assert isinstance(resolve_default_embeddings(), HashEmbeddings)


def test_resolve_default_embeddings_forced_hash(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
monkeypatch.setenv("SYNTHORA_EMBEDDINGS", "hash")
assert isinstance(resolve_default_embeddings(), HashEmbeddings)


def test_resolve_default_embeddings_prefers_openai_when_keyed(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
monkeypatch.delenv("SYNTHORA_EMBEDDINGS", raising=False)
assert isinstance(resolve_default_embeddings(), OpenAIEmbeddings)


def test_resolve_default_embeddings_uses_ollama_when_configured(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("SYNTHORA_EMBEDDINGS", raising=False)
monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
assert isinstance(resolve_default_embeddings(), OllamaEmbeddings)
37 changes: 37 additions & 0 deletions tests/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,43 @@ def test_sdk_health_and_ready(sdk):
assert sdk.ready()["status"] == "ready"


def test_sync_events_ws_url(sdk):
sdk.token = "tok"
url = sdk.events_ws_url("run-1")
assert "/api/v1/research/run-1/events/ws" in url
assert "token=tok" in url


def test_sync_iter_run_events(monkeypatch):
from websockets.exceptions import ConnectionClosedOK

payloads = [
{"type": "status", "message": "queued"},
{"type": "done", "message": "completed"},
]

class _FakeWS:
def __enter__(self):
return self

def __exit__(self, *_args):
return False

def recv(self):
if payloads:
return json.dumps(payloads.pop(0))
raise ConnectionClosedOK(None, None)

monkeypatch.setattr(
"websockets.sync.client.connect",
lambda *_a, **_k: _FakeWS(),
)
client = SynthoraClient("http://localhost:8000")
events = list(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
Expand Down
Loading