Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
39f6f8a
feat(agent): bootstrap FastAPI agent service
Zawiszowski Apr 2, 2026
2cd3ae1
refactor(agent): align data model with spec
FBegiello Apr 11, 2026
cfa20e8
feat(agent): add initial alembic migration
FBegiello Apr 11, 2026
a68926b
feat(agent): add pygentic-ai with multi-provider LLM support
FBegiello Apr 11, 2026
63764bb
feat(backend): implement recovery endpoint and extend user profile
FBegiello Apr 11, 2026
6d00e6d
feat(agent): implement OW backend REST client
FBegiello Apr 11, 2026
d7121eb
feat(agent): implement agent toolsets, prompts, and workflow engine
FBegiello Apr 11, 2026
797fd75
feat(agent): wire workflow engine into process_message Celery task
FBegiello Apr 11, 2026
4a1c06e
feat(agent): implement conversation lifecycle Celery worker
FBegiello Apr 11, 2026
24c9a68
chore: add .gitattributes for LF line endings and update scripts
FBegiello Apr 11, 2026
12da7ad
test(agent): add pytest test suite covering all new features
FBegiello Apr 13, 2026
e61f69e
chore(agent): Language coding in line with ISO 629 codes and minor cl…
FBegiello Apr 13, 2026
68a62a7
fix(agent): Prompt rework and general housekeeping
FBegiello Apr 13, 2026
df3ab90
chore(agent): Add language handling for default messages
FBegiello Apr 13, 2026
0ebb243
chore(agent): Change llm handling to cover defaults for each provider…
FBegiello Apr 13, 2026
9ece9ed
chore(agent): Add healthcheck for LLM providers
FBegiello Apr 13, 2026
1dfecad
chore(agent): Unified naming convention across the project with top t…
FBegiello Apr 13, 2026
34796c0
fix(agent): Fix migrations and dependencies for pygentic-ai
FBegiello Apr 13, 2026
c12a572
fix(agent): Fix default llm falbacks
FBegiello Apr 13, 2026
e973355
chore(agent): Fix all ruff and ty violations, remove dead ChatSession…
FBegiello Apr 13, 2026
5195c13
test(agent): refactor agent test suite for pydantic-ai workflow
Zawiszowski Apr 14, 2026
b530383
fix(agent): convert fixtures and tests to async and fix validation er…
Zawiszowski Apr 14, 2026
76d9505
chore(agent): remove unused celery tasks: dummy_task and expire_sessions
Zawiszowski Apr 14, 2026
7ec22a8
fix(agent): rename auth header to X-Open-Wearables-API-Key in OWClient
Zawiszowski Apr 14, 2026
61759d9
chore(agent): delete unused filter and error schemas
Zawiszowski Apr 14, 2026
38445ca
refactor(agent): align exception handling with backend pattern
Zawiszowski Apr 14, 2026
716fc44
fix(agent): include assistant messages in chat history seed
FBegiello Apr 14, 2026
7522328
fix(agent): replace manual ReAct prompt with pydantic-ai-compatible g…
FBegiello Apr 14, 2026
55eede3
fix(agent): cap tool calls per turn via UsageLimits from config
FBegiello Apr 14, 2026
ff45a5e
fix(agent): inject user_id via RunContext deps instead of message text
FBegiello Apr 14, 2026
5033074
fix(router): pass chat history to router for contextual follow-up cla…
FBegiello Apr 15, 2026
e077e5b
fix(router): emit short refusals in the configured conversation language
FBegiello Apr 15, 2026
b0a84fe
fix(agent): address CodeRabbit review issues from PR #765
FBegiello Apr 15, 2026
ff50825
fix(agent): address second batch of CodeRabbit review issues from PR …
FBegiello Apr 15, 2026
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
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Default: normalize line endings to LF on commit
* text=auto eol=lf

# Shell scripts must always be LF
*.sh text eol=lf

# Windows-only files can keep CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
24 changes: 21 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,32 @@ repos:
language: system
files: ^backend/.*\.py$
pass_filenames: false
- id: agent-ruff-check
name: agent ruff check
entry: sh -c 'cd agent && uv run ruff check --fix'
language: system
files: ^agent/.*\.py$
pass_filenames: false
- id: agent-ruff-format
name: agent ruff format
entry: sh -c 'cd agent && uv run ruff format'
language: system
files: ^agent/.*\.py$
pass_filenames: false
- id: agent-ty
name: agent ty check
entry: sh -c 'cd agent && uv run ty check .'
language: system
files: ^agent/.*\.py$
pass_filenames: false

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
files: ^(backend|mcp)/
files: ^(backend|agent|mcp)/
- id: end-of-file-fixer
files: ^(backend|mcp)/
files: ^(backend|agent|mcp)/
- id: check-merge-conflict
args: [--assume-in-merge]
files: ^(backend|mcp)/
files: ^(backend|agent|mcp)/
7 changes: 3 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/backend/.venv/bin/python",
"python.defaultInterpreterPath": "${workspaceFolder}/agent/.venv/bin/python",
"python.analysis.extraPaths": [
"${workspaceFolder}/backend"
"${workspaceFolder}/agent"
],
"makefile.configureOnOpen": false
}

}
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
DOCKER_COMMAND = docker compose -f docker-compose.yml
DOCKER_EXEC = $(DOCKER_COMMAND) exec app
DOCKER_EXEC_AGENT = $(DOCKER_COMMAND) exec agent
ALEMBIC_CMD = uv run alembic

help: ## Show this help.
Expand Down Expand Up @@ -48,3 +49,19 @@ downgrade: ## Revert the last migration

reset_db: ## Truncate all tables in the database (WARNING: deletes all data)
$(DOCKER_EXEC) uv run python scripts/reset_database.py

agent-test: ## Run agent tests.
cd agent && uv run pytest -v --cov=app

agent-migrate: ## Apply all agent migrations
$(DOCKER_EXEC_AGENT) $(ALEMBIC_CMD) upgrade head

agent-create_migration: ## Create a new agent migration. Use 'make agent-create_migration m="Description of the change"'
@if [ -z "$(m)" ]; then \
echo "Error: You must provide a migration description using 'm=\"Description\"'"; \
exit 1; \
fi
$(DOCKER_EXEC_AGENT) $(ALEMBIC_CMD) revision --autogenerate -m "$(m)"

agent-downgrade: ## Revert the last agent migration
$(DOCKER_EXEC_AGENT) $(ALEMBIC_CMD) downgrade -1
1 change: 1 addition & 0 deletions agent/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
43 changes: 43 additions & 0 deletions agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
FROM python:3.13-slim AS builder

RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential libpq-dev git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir --upgrade pyopenssl

COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/

WORKDIR /root_project

COPY uv.lock pyproject.toml ./
COPY . .

ENV VIRTUAL_ENV=/opt/venv
ENV UV_PROJECT_ENVIRONMENT=/opt/venv
RUN uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN uv sync --locked --no-dev

FROM python:3.13-slim

RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Comment on lines +24 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if git is used in startup scripts or at runtime
rg -n --type sh -C3 'git\s' agent/scripts/

Repository: the-momentum/open-wearables

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Read the full Dockerfile to understand the build context
cat -n agent/Dockerfile

Repository: the-momentum/open-wearables

Length of output: 1383


🏁 Script executed:

#!/bin/bash
# Check for git-based dependencies in pyproject.toml or requirements files
fd -e toml -e txt -e lock | grep -E '(pyproject|requirements|uv\.lock)' | head -20

Repository: the-momentum/open-wearables

Length of output: 201


🏁 Script executed:

#!/bin/bash
# Search for pygentic-ai and how it's referenced
rg -n 'pygentic-ai' --max-count 20

Repository: the-momentum/open-wearables

Length of output: 1769


Remove git from the runtime stage to reduce image size and attack surface.

The pygentic-ai dependency is installed from a git repository, but git is only needed during the build stage (in the builder). Since all dependencies are pre-installed in the builder's venv and copied to runtime as-is, git is not required at runtime.

🗑️ Proposed fix to remove git from runtime
     RUN apt-get update && \
-    apt-get install -y --no-install-recommends libpq5 git && \
+    apt-get install -y --no-install-recommends libpq5 && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent/Dockerfile` around lines 24 - 27, The runtime Dockerfile currently
installs git in the RUN command (the apt-get install -y --no-install-recommends
libpq5 git step) which is unnecessary at runtime; remove "git" from that install
line so only libpq5 is installed in the final stage, keeping git limited to the
builder stage where pygentic-ai is cloned and dependencies are prepared.


WORKDIR /root_project

COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/

COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /root_project /root_project
ENV VIRTUAL_ENV=/opt/venv
ENV PATH="/opt/venv/bin:$PATH"
ENV UV_PROJECT_ENVIRONMENT=/opt/venv

RUN groupadd -r appuser && useradd -r -g appuser appuser && \
chown -R appuser:appuser /root_project /opt/venv
USER appuser

EXPOSE 8000
Comment thread
coderabbitai[bot] marked this conversation as resolved.
57 changes: 57 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# API

A FastAPI application with PostgreSQL database support, containerized with Docker.

## Prerequisites

- Docker and Docker Compose
- Python 3.12+ (for local development)

## Setup

1. **Create environment file**
```bash
cp ./config/.env.example ./config/.env
# Edit .env file with your configuration
```

## Running the Application

### Docker (Recommended)

```bash
# Start services
docker compose up -d

# Create migration
docker compose exec app uv run alembic revision --autogenerate -m "Description"

# Run migrations
docker compose exec app uv run alembic upgrade head
```

### Local Development

```bash
# Install dependencies
uv sync

# Start PostgreSQL locally

# Create migration
uv run alembic revision --autogenerate -m "Description"

# Run migrations
uv run alembic upgrade head

# Start development server
uv run fastapi run app/main.py --reload
```

### Access the application
- API: http://localhost:8000
- Swagger: http://localhost:8000/docs

---

This project was generated from the [Python AI Kit](https://github.com/the-momentum/python-ai-kit).
39 changes: 39 additions & 0 deletions agent/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[alembic]
script_location = migrations
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url =

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Empty file added agent/app/__init__.py
Empty file.
Empty file added agent/app/agent/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions agent/app/agent/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Agent dependency types for pydantic-ai RunContext injection."""

from __future__ import annotations

from dataclasses import dataclass
from uuid import UUID

from pygentic_ai.engines.base import BaseAgentDeps


@dataclass
class HealthAgentDeps(BaseAgentDeps):
"""Dependencies injected into every tool call via RunContext.

Extends BaseAgentDeps (which carries language) with the resolved
user_id so tools never need to receive it as a model-supplied argument.
"""

user_id: UUID | None = None
Empty file.
38 changes: 38 additions & 0 deletions agent/app/agent/engines/guardrails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Health guardrails agent — wraps pygentic-ai GuardrailsAgent."""

from __future__ import annotations

from pygentic_ai.engines.guardrails import GuardrailsAgent

from app.agent.prompts.worker_prompts import WorkerType, build_worker_prompt
from app.agent.utils.model_utils import get_llm

_DEFAULT_SOFT_WORD_LIMIT = 150


class HealthGuardrailsAgent(GuardrailsAgent):
"""Output formatter and validator for health assistant responses.

Wraps pygentic-ai GuardrailsAgent with health-domain formatting
rules and the configured worker LLM from app settings.
"""

def __init__(
self,
language: str = "English",
soft_word_limit: int | None = _DEFAULT_SOFT_WORD_LIMIT,
) -> None:
vendor, model, api_key = get_llm(is_worker=True)
prompt = build_worker_prompt(
WorkerType.GUARDRAILS,
language=language,
soft_word_limit=soft_word_limit,
)

super().__init__(
llm_vendor=vendor,
llm_model=model,
api_key=api_key,
system_prompt=prompt,
language=language,
)
68 changes: 68 additions & 0 deletions agent/app/agent/engines/reasoning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Health-domain reasoning agent — wraps pygentic-ai BaseAgent."""

from __future__ import annotations

from typing import Any
from uuid import UUID

from pydantic_ai import UsageLimits
from pydantic_ai.messages import ModelMessage
from pydantic_ai.run import AgentRunResult
from pygentic_ai import BaseAgent

from app.agent.deps import HealthAgentDeps
from app.agent.prompts.agent_prompts import build_system_prompt
from app.agent.utils.model_utils import get_llm
from app.config import settings
from app.schemas.agent import AgentMode
from app.schemas.language import LANGUAGE_NAMES, Language


class HealthReasoningAgent(BaseAgent):
"""ReAct-style reasoning agent for the Open Wearables health domain.

Wraps pygentic-ai BaseAgent with health-specific instructions and
the configured LLM provider from app settings.

user_id is stored on the instance and injected into every tool call
via HealthAgentDeps / RunContext — the model never needs to supply it.
"""

def __init__(
self,
user_id: UUID,
mode: AgentMode = AgentMode.GENERAL,
tools: list | None = None,
language: Language | None = None,
) -> None:
self.user_id = user_id
vendor, model, api_key = get_llm()
lang_name = LANGUAGE_NAMES[language] if language else LANGUAGE_NAMES[Language.english]
instructions = build_system_prompt(mode, language)

super().__init__(
llm_vendor=vendor,
llm_model=model,
api_key=api_key,
tool_list=tools or [],
system_prompt=instructions,
language=lang_name,
deps_type=HealthAgentDeps,
usage_limits=UsageLimits(request_limit=settings.max_tool_calls),
)

async def generate_response(
self,
query: str,
chat_history: list[ModelMessage] | None = None,
) -> AgentRunResult:
"""Generate response, injecting user_id into deps for tool access."""
deps = HealthAgentDeps(language=self.language, user_id=self.user_id)
run_kwargs: dict[str, Any] = {
"user_prompt": query,
"message_history": chat_history or [],
"deps": deps,
}
if self.usage_limits:
run_kwargs["usage_limits"] = self.usage_limits
return await self.agent.run(**run_kwargs)
Loading