{"id":46683882,"url":"https://github.com/garybake/change_seams","last_synced_at":"2026-03-09T01:01:20.260Z","repository":{"id":342230141,"uuid":"1165122083","full_name":"garybake/change_seams","owner":"garybake","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-05T08:57:58.000Z","size":47,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-05T13:07:07.304Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/garybake.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-23T21:07:16.000Z","updated_at":"2026-02-25T23:46:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/garybake/change_seams","commit_stats":null,"previous_names":["garybake/change_seams"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/garybake/change_seams","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garybake%2Fchange_seams","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garybake%2Fchange_seams/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garybake%2Fchange_seams/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garybake%2Fchange_seams/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/garybake","download_url":"https://codeload.github.com/garybake/change_seams/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/garybake%2Fchange_seams/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30279765,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T20:45:49.896Z","status":"ssl_error","status_checked_at":"2026-03-08T20:45:49.525Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-09T01:01:18.649Z","updated_at":"2026-03-09T01:01:19.987Z","avatar_url":"https://github.com/garybake.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Change Seams\n\nA FastAPI reference app demonstrating how to build an LLM application where the provider, prompts, tools, config, and observability are each a **change seam** — a narrow interface you can swap without touching anything else.\n\nSee blog post [https://garybake.com/seams1.html](https://garybake.com/seams1.html)\n\n```\nswap provider  →  change LLM_PROVIDER=anthropic in .env, restart\nswap prompt    →  PUT /api/prompts/agent.system/2/activate\ndisable tools  →  change ENABLED_TOOLS=echo in .env, restart\nswap tracer    →  change one line in app/observability/tracing.py\n```\n\nThe UI is a single-page chat interface with a live trace panel showing spans, tool calls, and token usage for every request.\n\n---\n\n## Prerequisites\n\n- Docker and Docker Compose\n- An OpenAI API key (required)\n- Optionally: a [Tavily](https://tavily.com) key for web search and an [OpenWeatherMap](https://openweathermap.org/api) key for weather\n\n---\n\n## Setup\n\n**1. Clone and configure**\n\n```bash\ngit clone \u003crepo\u003e\ncd change_seams\ncp .env.example .env\n```\n\nEdit `.env` and set at minimum:\n\n```dotenv\nOPENAI_API_KEY=sk-...\n```\n\nOptionally enable more tools:\n\n```dotenv\nTAVILY_API_KEY=tvly-...\nOPENWEATHERMAP_API_KEY=...\nENABLED_TOOLS=echo,weather,search\n```\n\n**2. Start the app**\n\n```bash\ndocker-compose up\n```\n\nThis starts Postgres, runs Alembic migrations (including a seed prompt), and serves the app with hot-reload at `http://localhost:8000`.\n\n**3. Open the UI**\n\nVisit `http://localhost:8000`. Try asking:\n\n- *\"What is the square root of 144?\"* — tests the echo/reasoning path\n- *\"What's the weather in Dublin?\"* — invokes the weather tool\n- *\"What happened in the news today?\"* — invokes web search\n\nThe trace panel on the right shows every span: LLM calls, tool invocations, latency, and token counts.\n\n---\n\n## Running tests\n\nNo API keys or running Postgres needed — tests use SQLite and mock all LLM calls.\n\n```bash\npip install -r requirements.txt\npytest tests/\n```\n\n---\n\n## The five seams\n\n### 1. Provider (`app/providers/llm.py`)\n\n`get_llm()` reads `LLM_PROVIDER`, `LLM_MODEL`, and `LLM_TEMPERATURE` from the environment and returns the appropriate LangChain chat model. Adding a provider is one `elif` branch.\n\n```dotenv\nLLM_PROVIDER=openai        # or anthropic\nLLM_MODEL=gpt-4o-mini\nLLM_TEMPERATURE=0.7\n```\n\n### 2. Prompt registry (`app/prompts/`)\n\nPrompts are stored in Postgres with versioning. The agent fetches the active prompt by key on every request — no restart needed to change behaviour.\n\n```bash\n# Create a new version\ncurl -X POST http://localhost:8000/api/prompts \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"key\":\"agent.system\",\"content\":\"You are a pirate assistant.\",\"owner\":\"me\"}'\n\n# Activate it (response takes effect immediately)\ncurl -X PUT http://localhost:8000/api/prompts/agent.system/2/activate\n```\n\n### 3. Tool contract layer (`app/tools/`)\n\nEach tool declares a `ToolContract`: name, description, JSON Schema args, and required permissions. The `ENABLED_TOOLS` env var gates which tools the agent can call.\n\n```dotenv\nENABLED_TOOLS=echo,weather,search   # remove any to disable it live\n```\n\nTools handle missing API keys gracefully — they return an explanatory message rather than crashing.\n\n### 4. Runtime config (`app/config.py`)\n\nAll flags are read from the environment via Pydantic Settings. The `/api/config` endpoint exposes the current values (no secrets).\n\n```bash\ncurl http://localhost:8000/api/config\n```\n\n### 5. Observability (`app/observability/tracing.py`)\n\nEvery request creates an OpenTelemetry root span. Child spans are emitted for each LLM call and tool invocation. Spans are exported to stdout (console) and also returned in the API response so the trace panel can render them without a separate backend.\n\nTo point at a real collector, swap `ConsoleSpanExporter` for an OTLP exporter in `setup_tracing()`.\n\n---\n\n## API reference\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/api/chat` | Run agent; returns answer + trace spans |\n| `GET` | `/api/config` | Current runtime config (no secrets) |\n| `GET` | `/api/prompts` | List latest version of every prompt |\n| `GET` | `/api/prompts/{key}` | All versions for a prompt key |\n| `POST` | `/api/prompts` | Create a new prompt version |\n| `PUT` | `/api/prompts/{key}/{version}/activate` | Activate a version |\n| `DELETE` | `/api/prompts/{id}` | Delete a prompt row |\n| `GET` | `/health` | Liveness check |\n| `GET` | `/docs` | Swagger UI |\n\n---\n\n## Project structure\n\n```\napp/\n  config.py              # Seam 4 — Pydantic Settings, reads .env\n  db.py                  # Async SQLAlchemy engine + session\n  models/                # PromptTemplate, ObservationLog ORM models\n  providers/llm.py       # Seam 1 — get_llm() factory\n  tools/                 # Seam 3 — echo, weather, search + registry\n  prompts/               # Seam 2 — registry service + CRUD router\n  observability/         # Seam 5 — OTEL setup + OtelCallbackHandler\n  agent/runner.py        # Wires all 5 seams per request\n  api/chat.py            # POST /api/chat, GET /api/config\n  main.py                # FastAPI app, lifespan, static file mount\nfrontend/                # index.html, app.js, style.css (vanilla JS)\nalembic/versions/        # DB migrations + default prompt seed\ntests/                   # 33 tests, zero real API calls\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgarybake%2Fchange_seams","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgarybake%2Fchange_seams","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgarybake%2Fchange_seams/lists"}