-
Notifications
You must be signed in to change notification settings - Fork 417
feat(agent): bootstrap FastAPI agent service #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 32 commits
39f6f8a
2cd3ae1
cfa20e8
a68926b
63764bb
6d00e6d
d7121eb
797fd75
4a1c06e
24c9a68
12da7ad
e61f69e
68a62a7
df3ab90
0ebb243
9ece9ed
1dfecad
34796c0
c12a572
e973355
5195c13
b530383
76d9505
7ec22a8
61759d9
38445ca
716fc44
7522328
55eede3
ff45a5e
5033074
e077e5b
b0a84fe
ff50825
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 3.13 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||||||||||
| 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:latest /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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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/DockerfileRepository: 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 -20Repository: 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 20Repository: the-momentum/open-wearables Length of output: 1769 Remove The 🗑️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| WORKDIR /root_project | ||||||||||||||||||
|
|
||||||||||||||||||
| COPY --from=ghcr.io/astral-sh/uv:latest /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 | ||||||||||||||||||
|
|
||||||||||||||||||
| EXPOSE 8000 | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
| 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). |
| 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 |
| 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 |
| 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, | ||
| ) |
| 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) |
Uh oh!
There was an error while loading. Please reload this page.