diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..42effa95 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Timmy Time — Mission Control +# Copy this file to .env and uncomment lines you want to override. +# .env is gitignored and never committed. + +# Ollama host (default: http://localhost:11434) +# Override if Ollama is running on another machine or port. +# OLLAMA_URL=http://localhost:11434 + +# LLM model to use via Ollama (default: llama3.2) +# OLLAMA_MODEL=llama3.2 + +# Enable FastAPI interactive docs at /docs and /redoc (default: false) +# DEBUG=true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..02fd1748 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,25 @@ +name: Tests + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: Install dependencies + run: pip install -e ".[dev]" + + - name: Run tests + run: pytest --cov=src --cov-report=term-missing diff --git a/.gitignore b/.gitignore index 54d38079..7415281b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,10 @@ dist/ venv/ env/ -# Secrets / local config +# Secrets / local config — commit only .env.example (the template) .env .env.* +!.env.example # SQLite memory — never commit agent memory *.db diff --git a/pyproject.toml b/pyproject.toml index b43ef5e2..f659f1b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "aiofiles>=24.0.0", "typer>=0.12.0", "rich>=13.0.0", + "pydantic-settings>=2.0.0", ] [project.optional-dependencies] @@ -32,7 +33,8 @@ dev = [ timmy = "timmy.cli:main" [tool.hatch.build.targets.wheel] -packages = ["src/timmy", "src/dashboard"] +sources = {"src" = ""} +include = ["src/timmy", "src/dashboard", "src/config.py"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/config.py b/src/config.py new file mode 100644 index 00000000..ec387d3c --- /dev/null +++ b/src/config.py @@ -0,0 +1,21 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + # Ollama host — override with OLLAMA_URL env var or .env file + ollama_url: str = "http://localhost:11434" + + # LLM model passed to Agno/Ollama — override with OLLAMA_MODEL + ollama_model: str = "llama3.2" + + # Set DEBUG=true to enable /docs and /redoc (disabled by default) + debug: bool = False + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + +settings = Settings() diff --git a/src/dashboard/app.py b/src/dashboard/app.py index b71a78be..91312b5d 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from fastapi import FastAPI, Request @@ -5,13 +6,27 @@ from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from config import settings from dashboard.routes.agents import router as agents_router from dashboard.routes.health import router as health_router +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(name)s — %(message)s", + datefmt="%H:%M:%S", +) +logger = logging.getLogger(__name__) + BASE_DIR = Path(__file__).parent PROJECT_ROOT = BASE_DIR.parent.parent -app = FastAPI(title="Timmy Time — Mission Control", version="1.0.0") +app = FastAPI( + title="Timmy Time — Mission Control", + version="1.0.0", + # Docs disabled unless DEBUG=true in env / .env + docs_url="/docs" if settings.debug else None, + redoc_url="/redoc" if settings.debug else None, +) templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) app.mount("/static", StaticFiles(directory=str(PROJECT_ROOT / "static")), name="static") diff --git a/src/dashboard/routes/health.py b/src/dashboard/routes/health.py index 4783a4c9..05968e72 100644 --- a/src/dashboard/routes/health.py +++ b/src/dashboard/routes/health.py @@ -4,17 +4,17 @@ from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from pathlib import Path +from config import settings + router = APIRouter(tags=["health"]) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) -OLLAMA_URL = "http://localhost:11434" - async def check_ollama() -> bool: """Ping Ollama to verify it's running.""" try: async with httpx.AsyncClient(timeout=2.0) as client: - r = await client.get(OLLAMA_URL) + r = await client.get(settings.ollama_url) return r.status_code == 200 except Exception: return False diff --git a/src/timmy/agent.py b/src/timmy/agent.py index af4a11fe..cc1aab3b 100644 --- a/src/timmy/agent.py +++ b/src/timmy/agent.py @@ -3,13 +3,14 @@ from agno.models.ollama import Ollama from agno.db.sqlite import SqliteDb from timmy.prompts import TIMMY_SYSTEM_PROMPT +from config import settings def create_timmy(db_file: str = "timmy.db") -> Agent: """Instantiate Timmy with Agno + Ollama + SQLite memory.""" return Agent( name="Timmy", - model=Ollama(id="llama3.2"), + model=Ollama(id=settings.ollama_model), db=SqliteDb(db_file=db_file), description=TIMMY_SYSTEM_PROMPT, add_history_to_context=True,