From 303d33a1b33aace59368af5d83d7ab12afbc8820 Mon Sep 17 00:00:00 2001 From: Zac Pustejovsky Date: Tue, 12 May 2026 14:30:28 -0400 Subject: [PATCH 1/3] openrouter compat --- .../zeroshot_agentic_workflows/__init__.py | 2 ++ .../agent_service.py | 5 ++-- .../src/zeroshot_agentic_workflows/factory.py | 28 ++++++++++++------- ...ice_ollama.py => service_openai_compat.py} | 13 +++++---- .../tests/unit/test_factory.py | 21 +++++++++++--- uv.lock | 16 +++++------ 6 files changed, 55 insertions(+), 30 deletions(-) rename packages/agentic-workflows/src/zeroshot_agentic_workflows/{service_ollama.py => service_openai_compat.py} (91%) diff --git a/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py b/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py index b6746e6..b569a2d 100644 --- a/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py +++ b/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py @@ -15,6 +15,7 @@ from .decorators import agent, agentic_workflow, consensus_agent from .factory import AiAgentFactory from .param_mapper import AgentParameterMapper +from .service_openai_compat import AiAgentServiceOpenAICompat from .prompt_utils import ( ParsedPrompt, PromptFrontmatter, @@ -46,6 +47,7 @@ "AiAgentProvider", "AiAgentService", "AiAgentServiceLocal", + "AiAgentServiceOpenAICompat", "AiSessionFactory", "ConsensusRunResult", "ConsensusStrategy", diff --git a/packages/agentic-workflows/src/zeroshot_agentic_workflows/agent_service.py b/packages/agentic-workflows/src/zeroshot_agentic_workflows/agent_service.py index ac4db36..624a262 100644 --- a/packages/agentic-workflows/src/zeroshot_agentic_workflows/agent_service.py +++ b/packages/agentic-workflows/src/zeroshot_agentic_workflows/agent_service.py @@ -74,7 +74,7 @@ async def create_and_run( class AiAgentProvider(StrEnum): OPENAI = "openai" - OLLAMA = "ollama" + OPENAI_COMPAT = "openai_compat" @dataclass(frozen=True, slots=True) @@ -82,7 +82,8 @@ class AiAgentConfig: local: bool provider: AiAgentProvider = AiAgentProvider.OPENAI openai_api_token: str | None = None - ollama_base_url: str = "http://localhost:11434" + openai_compat_base_url: str | None = None + openai_compat_api_key: str | None = None default_model: str | None = None diff --git a/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py b/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py index 01f8e43..d21e484 100644 --- a/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py +++ b/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py @@ -1,8 +1,8 @@ from __future__ import annotations from .agent_service import AiAgentConfig, AiAgentProvider, AiAgentService, AiAgentServiceLocal -from .service_ollama import AiAgentServiceOllama from .service_openai import AiAgentServiceOpenai +from .service_openai_compat import AiAgentServiceOpenAICompat class AiAgentFactory: @@ -15,10 +15,13 @@ def make_agent_service(self) -> AiAgentService: if self._config.local: return AiAgentServiceLocal.get_instance() - if self._config.provider == AiAgentProvider.OLLAMA: - return AiAgentServiceOllama( - base_url=self._config.ollama_base_url, - default_model=self._config.default_model or "qwen2.5:14b", + if self._config.provider == AiAgentProvider.OPENAI_COMPAT: + if not self._config.openai_compat_base_url: + raise ValueError("openai_compat_base_url is required for the OpenAI-compatible provider") + return AiAgentServiceOpenAICompat( + base_url=self._config.openai_compat_base_url, + api_key=self._config.openai_compat_api_key or "", + default_model=self._config.default_model or "", ) if self._config.provider == AiAgentProvider.OPENAI: @@ -32,11 +35,16 @@ def make_agent_service(self) -> AiAgentService: raise ValueError(f"Unknown provider: {self._config.provider}") @staticmethod - def make_ollama_service( - base_url: str = "http://localhost:11434", - default_model: str = "qwen2.5:14b", - ) -> AiAgentServiceOllama: - return AiAgentServiceOllama(base_url=base_url, default_model=default_model) + def make_openai_compat_service( + base_url: str, + api_key: str, + default_model: str, + ) -> AiAgentServiceOpenAICompat: + return AiAgentServiceOpenAICompat( + base_url=base_url, + api_key=api_key, + default_model=default_model, + ) @staticmethod def make_openai_service( diff --git a/packages/agentic-workflows/src/zeroshot_agentic_workflows/service_ollama.py b/packages/agentic-workflows/src/zeroshot_agentic_workflows/service_openai_compat.py similarity index 91% rename from packages/agentic-workflows/src/zeroshot_agentic_workflows/service_ollama.py rename to packages/agentic-workflows/src/zeroshot_agentic_workflows/service_openai_compat.py index 2045545..da3c7df 100644 --- a/packages/agentic-workflows/src/zeroshot_agentic_workflows/service_ollama.py +++ b/packages/agentic-workflows/src/zeroshot_agentic_workflows/service_openai_compat.py @@ -13,19 +13,20 @@ logger = logging.getLogger(__name__) -class AiAgentServiceOllama: - """Ollama implementation via OpenAI-compatible API.""" +class AiAgentServiceOpenAICompat: + """OpenAI-compatible API implementation (Ollama, OpenRouter, etc.).""" def __init__( self, - base_url: str = "http://localhost:11434", - default_model: str = "qwen2.5:14b", + base_url: str, + api_key: str, + default_model: str, ) -> None: self._base_url = base_url self._default_model = default_model self._client = AsyncOpenAI( - base_url=f"{base_url}/v1", - api_key="ollama", + base_url=base_url, + api_key=api_key, ) def create_agent(self, config: AgentConfig[T]) -> AgentType[T]: diff --git a/packages/agentic-workflows/tests/unit/test_factory.py b/packages/agentic-workflows/tests/unit/test_factory.py index b6ec699..2d153f7 100644 --- a/packages/agentic-workflows/tests/unit/test_factory.py +++ b/packages/agentic-workflows/tests/unit/test_factory.py @@ -29,11 +29,24 @@ def test_openai_provider_requires_token(self) -> None: with pytest.raises(ValueError, match="openai_api_token"): factory.make_agent_service() - def test_static_ollama_factory(self) -> None: - from zeroshot_agentic_workflows.service_ollama import AiAgentServiceOllama + def test_static_openai_compat_factory(self) -> None: + from zeroshot_agentic_workflows.service_openai_compat import AiAgentServiceOpenAICompat - service = AiAgentFactory.make_ollama_service() - assert isinstance(service, AiAgentServiceOllama) + service = AiAgentFactory.make_openai_compat_service( + base_url="http://localhost:11434/v1", + api_key="test", + default_model="qwen2.5:14b", + ) + assert isinstance(service, AiAgentServiceOpenAICompat) + + def test_openai_compat_provider_requires_base_url(self) -> None: + config = AiAgentConfig( + local=False, + provider=AiAgentProvider.OPENAI_COMPAT, + ) + factory = AiAgentFactory(config) + with pytest.raises(ValueError, match="openai_compat_base_url"): + factory.make_agent_service() class TestAiSessionFactory: diff --git a/uv.lock b/uv.lock index d7bd476..b6b4cbe 100644 --- a/uv.lock +++ b/uv.lock @@ -101,7 +101,7 @@ wheels = [ [[package]] name = "buildkit-python-workspace" -version = "0.1.5" +version = "0.1.6" source = { virtual = "." } [package.dev-dependencies] @@ -1641,7 +1641,7 @@ wheels = [ [[package]] name = "zeroshot-agent-experiments" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/agent-experiments" } dependencies = [ { name = "fpdf2" }, @@ -1664,7 +1664,7 @@ requires-dist = [ [[package]] name = "zeroshot-agentic-workflows" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/agentic-workflows" } dependencies = [ { name = "openai-agents" }, @@ -1681,7 +1681,7 @@ requires-dist = [ [[package]] name = "zeroshot-commons" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/commons" } dependencies = [ { name = "pyyaml" }, @@ -1696,7 +1696,7 @@ requires-dist = [ [[package]] name = "zeroshot-commons-injectors" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/commons-injectors" } dependencies = [ { name = "asyncpg" }, @@ -1717,7 +1717,7 @@ requires-dist = [ [[package]] name = "zeroshot-commons-testing" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/commons-testing" } dependencies = [ { name = "testcontainers", extra = ["redis"] }, @@ -1732,7 +1732,7 @@ requires-dist = [ [[package]] name = "zeroshot-openai-utils" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/openai-utils" } dependencies = [ { name = "dependency-injector" }, @@ -1749,7 +1749,7 @@ requires-dist = [ [[package]] name = "zeroshot-sql-decorators" -version = "0.1.5" +version = "0.1.6" source = { editable = "packages/sql-decorators" } dependencies = [ { name = "asyncpg" }, From cc04d380a08a9b8b99dc4b90231910c6c4f0a1e6 Mon Sep 17 00:00:00 2001 From: Zac Pustejovsky Date: Tue, 12 May 2026 14:50:29 -0400 Subject: [PATCH 2/3] moar --- .../src/zeroshot_agentic_workflows/factory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py b/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py index d21e484..dd8d129 100644 --- a/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py +++ b/packages/agentic-workflows/src/zeroshot_agentic_workflows/factory.py @@ -17,7 +17,9 @@ def make_agent_service(self) -> AiAgentService: if self._config.provider == AiAgentProvider.OPENAI_COMPAT: if not self._config.openai_compat_base_url: - raise ValueError("openai_compat_base_url is required for the OpenAI-compatible provider") + raise ValueError( + "openai_compat_base_url is required for the OpenAI-compatible provider" + ) return AiAgentServiceOpenAICompat( base_url=self._config.openai_compat_base_url, api_key=self._config.openai_compat_api_key or "", From 62415e7e73fef1f38d08d0e113c9df82dc102a86 Mon Sep 17 00:00:00 2001 From: Zac Pustejovsky Date: Tue, 12 May 2026 15:06:05 -0400 Subject: [PATCH 3/3] moar --- .ai/ai-agents.md | 7 ++++--- .ai/skills/testing-guide.md | 2 +- .ai/testing.md | 2 +- packages/agent-experiments/assets/config.yaml | 2 +- packages/agent-experiments/tests/evals/conftest.py | 7 ++++--- .../tests/evals/test_salary_extraction.py | 6 +++--- .../src/zeroshot_agentic_workflows/__init__.py | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.ai/ai-agents.md b/.ai/ai-agents.md index ba651e2..8f412c1 100644 --- a/.ai/ai-agents.md +++ b/.ai/ai-agents.md @@ -10,7 +10,7 @@ multiple implementations: - `service_openai.py`: production implementation using the OpenAI Agents SDK - `agent_service.py` (`AiAgentServiceLocal`): mocked implementation for testing non-AI portions of workflows (orchestration, task management, error handling, etc) -- `service_ollama.py`: hits a local Ollama deployment for development/testing with local models +- `service_openai_compat.py`: hits any OpenAI-compatible API (Ollama, OpenRouter, etc.) ## Factory @@ -21,8 +21,9 @@ from zeroshot_agentic_workflows import AiAgentConfig, AiAgentFactory, AiAgentPro config = AiAgentConfig( local=False, - provider=AiAgentProvider.OLLAMA, - ollama_base_url="http://localhost:11434", + provider=AiAgentProvider.OPENAI_COMPAT, + openai_compat_base_url="http://localhost:11434/v1", + openai_compat_api_key="ollama", default_model="qwen2.5:latest", ) factory = AiAgentFactory(config) diff --git a/.ai/skills/testing-guide.md b/.ai/skills/testing-guide.md index ff7fbed..240764e 100644 --- a/.ai/skills/testing-guide.md +++ b/.ai/skills/testing-guide.md @@ -14,7 +14,7 @@ Read these docs for full details: ### Test Tiers - **Unit** (`tests/unit/`): always runs, no I/O - **Integration** (`tests/integration/`): `--integration` flag, real Postgres/Redis via testcontainers -- **Eval** (`tests/evals/`): `--eval` flag, real LLM inference via Ollama +- **Eval** (`tests/evals/`): `--eval` flag, real LLM inference via OpenAI-compatible API ### Testcontainers ```python diff --git a/.ai/testing.md b/.ai/testing.md index 911b91c..200386a 100644 --- a/.ai/testing.md +++ b/.ai/testing.md @@ -8,7 +8,7 @@ Tests are organized into three tiers, each gated by a CLI flag: |------|-----------|------|---------------| | Unit | `tests/unit/` | *(always runs)* | Pure logic, no I/O | | Integration | `tests/integration/` | `--integration` | Real Postgres/Redis via testcontainers | -| Eval | `tests/evals/` | `--eval` | Real LLM inference via Ollama | +| Eval | `tests/evals/` | `--eval` | Real LLM inference via OpenAI-compatible API | Run them: ```bash diff --git a/packages/agent-experiments/assets/config.yaml b/packages/agent-experiments/assets/config.yaml index 043588e..167aeac 100644 --- a/packages/agent-experiments/assets/config.yaml +++ b/packages/agent-experiments/assets/config.yaml @@ -1,3 +1,3 @@ -provider: ollama +provider: openai_compat defaultModel: "qwen2.5:latest" port: 3000 diff --git a/packages/agent-experiments/tests/evals/conftest.py b/packages/agent-experiments/tests/evals/conftest.py index c100203..d063299 100644 --- a/packages/agent-experiments/tests/evals/conftest.py +++ b/packages/agent-experiments/tests/evals/conftest.py @@ -12,11 +12,12 @@ @pytest_asyncio.fixture(scope="module", loop_scope="module") async def salary_agent(): - """Create a SalaryExtractionAgent wired to local Ollama.""" + """Create a SalaryExtractionAgent wired to a local OpenAI-compatible API.""" config = AiAgentConfig( local=False, - provider=AiAgentProvider.OLLAMA, - ollama_base_url="http://localhost:11434", + provider=AiAgentProvider.OPENAI_COMPAT, + openai_compat_base_url="http://localhost:11434/v1", + openai_compat_api_key="ollama", default_model="qwen2.5:latest", ) factory = AiAgentFactory(config) diff --git a/packages/agent-experiments/tests/evals/test_salary_extraction.py b/packages/agent-experiments/tests/evals/test_salary_extraction.py index d53baeb..44c285d 100644 --- a/packages/agent-experiments/tests/evals/test_salary_extraction.py +++ b/packages/agent-experiments/tests/evals/test_salary_extraction.py @@ -1,12 +1,12 @@ """Salary extraction eval suite. -These tests exercise real LLM inference via Ollama and are skipped -by default. Run with: +These tests exercise real LLM inference via an OpenAI-compatible API +and are skipped by default. Run with: uv run pytest --eval packages/agent-experiments/tests/evals/ Requires: - - Ollama running locally with ``qwen2.5:latest`` pulled + - An OpenAI-compatible API running (e.g. Ollama with ``qwen2.5:latest``) - PDF fixtures generated (run generate_fixtures.py first) """ diff --git a/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py b/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py index b569a2d..627453c 100644 --- a/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py +++ b/packages/agentic-workflows/src/zeroshot_agentic_workflows/__init__.py @@ -15,13 +15,13 @@ from .decorators import agent, agentic_workflow, consensus_agent from .factory import AiAgentFactory from .param_mapper import AgentParameterMapper -from .service_openai_compat import AiAgentServiceOpenAICompat from .prompt_utils import ( ParsedPrompt, PromptFrontmatter, generate_tools_reference, parse_prompt_frontmatter, ) +from .service_openai_compat import AiAgentServiceOpenAICompat from .session import ( CONVERSATION_SESSION_REPOSITORY, ConversationItemModel,