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
7 changes: 4 additions & 3 deletions .ai/ai-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion .ai/skills/testing-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .ai/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-experiments/assets/config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
provider: ollama
provider: openai_compat
defaultModel: "qwen2.5:latest"
port: 3000
7 changes: 4 additions & 3 deletions packages/agent-experiments/tests/evals/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
generate_tools_reference,
parse_prompt_frontmatter,
)
from .service_openai_compat import AiAgentServiceOpenAICompat
from .session import (
CONVERSATION_SESSION_REPOSITORY,
ConversationItemModel,
Expand All @@ -46,6 +47,7 @@
"AiAgentProvider",
"AiAgentService",
"AiAgentServiceLocal",
"AiAgentServiceOpenAICompat",
"AiSessionFactory",
"ConsensusRunResult",
"ConsensusStrategy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,16 @@ async def create_and_run(

class AiAgentProvider(StrEnum):
OPENAI = "openai"
OLLAMA = "ollama"
OPENAI_COMPAT = "openai_compat"


@dataclass(frozen=True, slots=True)
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


Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -15,10 +15,15 @@ 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:
Expand All @@ -32,11 +37,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
21 changes: 17 additions & 4 deletions packages/agentic-workflows/tests/unit/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 8 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.