{"id":50854478,"url":"https://github.com/kaiser-data/carlover","last_synced_at":"2026-06-14T17:06:35.216Z","repository":{"id":352025190,"uuid":"1213524387","full_name":"kaiser-data/carlover","owner":"kaiser-data","description":"Multi-agent automotive AI assistant — LangGraph + ADAC Pannenstatistik + Featherless AI, deployable on Daytona","archived":false,"fork":false,"pushed_at":"2026-04-17T15:21:34.000Z","size":397,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-17T15:43:18.653Z","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/kaiser-data.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-04-17T13:25:36.000Z","updated_at":"2026-04-17T15:21:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kaiser-data/carlover","commit_stats":null,"previous_names":["kaiser-data/carlover"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/kaiser-data/carlover","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiser-data%2Fcarlover","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiser-data%2Fcarlover/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiser-data%2Fcarlover/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiser-data%2Fcarlover/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kaiser-data","download_url":"https://codeload.github.com/kaiser-data/carlover/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaiser-data%2Fcarlover/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34329783,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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-06-14T17:06:34.497Z","updated_at":"2026-06-14T17:06:35.210Z","avatar_url":"https://github.com/kaiser-data.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CarLover 🚗\n\nA multi-agent automotive assistant. Ask a natural-language question about your car, optionally upload a photo, and get a sourced diagnosis backed by live ADAC reliability data, your own service history, and a vision pipeline.\n\n**Live demo:** https://8000-bfa16ffa-8c1c-45c1-91b5-ae17fbd72b23.daytonaproxy01.eu/ui/\n**API base:** same host, no `/ui/` suffix. OpenAPI at `/docs`, ReDoc at `/redoc`.\n\n---\n\n## What it does\n\nType *\"My BMW 1er 2020 squeaks when I brake\"*, optionally drag in a dashboard photo, and the backend:\n\n1. Classifies intent and extracts `{make, model, year, variant}` with typo-tolerant matching.\n2. Runs up to four specialist agents in parallel (ADAC, Supabase, Image, Sandbox).\n3. Merges the results and synthesises a German-language answer with citations, confidence, and follow-up questions when data is incomplete.\n\nIf the image has multiple cars the UI shows clarification cards with bounding boxes; if the inferred make/model is ambiguous the pipeline asks rather than guesses.\n\n---\n\n## Architecture\n\n```\n            ┌─────────────────────────────────────────────────┐\n            │  FastAPI app (lifespan warms HF models,         │\n            │  serves /ui static, mounts /chat /vehicle/...)  │\n            └─────────────────────────────────────────────────┘\n                                   │\n                                   ▼\n                         ┌─────────────────┐\n                         │  LangGraph      │\n                         │  StateGraph     │\n                         └─────────────────┘\n                                   │\n  intake ─► classify_intent ─► extract_entities ─► check_required_fields\n                                                           │\n                         needs_clarification?  ─── yes ──► clarify_if_needed ─► finalize\n                                                           │ no\n                                                           ▼\n                                                     route_agents\n                                                           │\n                                                           ▼\n                                     ┌─────────────────────────────────────┐\n                                     │ run_subagents (asyncio.gather)       │\n                                     ├─────────────────────────────────────┤\n                                     │  adac_agent       ADAC Autokatalog   │\n                                     │  supabase_agent   service history    │\n                                     │  image_agent      HF + VLM hybrid    │\n                                     │  sandbox_agent    Daytona ephemeral  │\n                                     └─────────────────────────────────────┘\n                                                           │\n                                                           ▼\n                                       merge_results ─► answer ─► finalize\n```\n\nEach box maps directly to a file:\n\n| Stage                | File                                       |\n|---|---|\n| Intake / graph nodes | `app/graph/nodes.py`, `app/graph/graph.py` |\n| Intent / entities    | `app/agents/orchestrator_agent.py`         |\n| ADAC                 | `app/agents/adac_agent.py`, `app/providers/adac/*` |\n| Supabase             | `app/agents/supabase_agent.py`, `app/providers/supabase/*` |\n| Image                | `app/agents/image_agent.py`, `app/services/car_detection.py` |\n| Sandbox              | `app/agents/sandbox_agent.py`, `app/providers/daytona/*`  |\n| Answer synthesis     | `app/agents/answer_agent.py`               |\n| LLM routing          | `app/providers/llm/model_router.py`        |\n\n### Image pipeline (hybrid)\n\nThe image agent runs **two paths concurrently** and merges them:\n\n- **Path A — HuggingFace Inference API** (deterministic, source of truth for identity)\n  - `facebook/detr-resnet-50` → car count + bounding boxes\n  - `dima806/car_models_image_detection` → make/model (369 classes, only called when DETR returns exactly one car)\n- **Path B — Featherless Qwen3-VL-235B-A22B-Thinking** (enrichment)\n  - Damage detection, warning-light names, prose observations, image-quality check\n\nEither path can fail and the response still renders. A tiny white PNG is fired at both HF models on app startup (`lifespan` in `app/main.py`) so the first real request is warm.\n\n### ADAC pipeline (three-tier fetch)\n\nDaytona sandboxes have a restrictive outbound allowlist — `adac.de` is blocked. `app/providers/adac/real_provider.py:_fetch_page` therefore tries, in order:\n\n1. **Supabase Edge Function** (`supabase/functions/adac-proxy/index.ts`) — always reachable from Daytona because the sandbox can talk to its own Supabase project. Enforces a host allowlist (`www.adac.de` only) at the edge.\n2. **ScraperAPI proxy** (when `SCRAPER_API_KEY` is set) — fallback for non-Daytona environments.\n3. **Direct fetch** — works from a developer laptop, blocked from the Daytona sandbox.\n\nParsing is identical in all three cases: extract `window.__staticRouterHydrationData`, navigate to `rangePage`, map to `ADACVehicleInfo` + `ADACIssuePattern[]`.\n\n---\n\n## Repository layout\n\n```\napp/\n  agents/            # one module per specialist agent\n  api/routes/        # FastAPI route handlers (health, chat, image, vehicle, debug)\n  graph/             # LangGraph state, nodes, and compiled graph\n  providers/         # adac/, daytona/, llm/, supabase/ — external I/O only\n  schemas/           # shared Pydantic models (requests, responses, vehicle, image)\n  services/          # car_detection.py — HF Inference API client\n  utils/             # vehicle_normalizer, etc.\n  main.py            # FastAPI app factory, lifespan, /ui static mount\nfrontend/\n  index.html         # single-file vanilla HTML/CSS/JS UI, mounted at /ui/\nsupabase/\n  functions/adac-proxy/index.ts  # Deno edge function for ADAC scraping\n  schema.sql, seed.sql           # DB schema + seed\nscripts/\n  deploy_daytona.py  # create/stop/status a Daytona sandbox\n  smoke_test.py      # end-to-end request against a running instance\n  run_eval.py        # replay eval_log.jsonl for regressions\n  seed_demo_data.py  # populate Supabase with demo rows\ntests/               # pytest suite — see Tests section\ndocs/                # architecture.md + auxiliary HTML\n```\n\n---\n\n## Backend API\n\nFive JSON endpoints, all CORS-open (`Access-Control-Allow-Origin: *`). Interactive docs at `/docs` (Swagger) and `/redoc`.\n\nSet `BASE` to either the live demo URL or `http://localhost:8000` for the examples below.\n\n### `GET /health`\n\n```json\n{ \"status\": \"ok\", \"version\": \"0.1.0\", \"timestamp\": \"2026-04-21T08:03:25Z\" }\n```\n\n### `POST /chat`\n\nRuns the full LangGraph pipeline.\n\n**Request**\n```ts\n{\n  query: string;                          // 1–2000 chars\n  vehicle?: { make: string; model: string; year?: number; variant?: string; vin?: string };\n  image_url?: string;                     // http(s) URL or data:image/...;base64,...\n  session_id?: string;                    // optional, for multi-turn conversations\n}\n```\n\n**Response**\n```ts\n{\n  request_id: string;\n  answer: string;                         // synthesized German-language answer\n  sources: Array\u003c{ label: string; type: \"adac\"|\"supabase\"|\"image\"|\"internal\"; confidence: number; url?: string }\u003e;\n  confidence: number;                     // 0–1\n  needs_clarification: boolean;\n  clarification_questions: string[];\n  used_agents: string[];                  // [\"adac\",\"database\",\"image\",\"sandbox\"]\n  debug_trace: Array\u003c{ node: string; elapsed_ms: number; note?: string }\u003e;  // only when DEBUG=true\n  elapsed_ms: number;\n  uncertainty_notes: string[];\n}\n```\n\n```bash\ncurl -X POST $BASE/chat \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"query\":\"My VW Golf 7 2017 squeaks when braking\"}'\n```\n\n### `POST /vehicle/lookup`\n\nTypo-tolerant ADAC lookup — `\"Vollkswagen Gollf\"` → VW Golf · `\"Polo\"` alone → VW Polo · `\"BMW 2er\"` in the model field → split correctly.\n\n**Request**\n```ts\n{ make?: string; model: string; year?: number }  // model required, min length 1\n```\n\n**Response**\n```ts\n{\n  normalized_make: string;\n  normalized_model: string;\n  year?: number;\n  corrections: string[];                  // human-readable inference/typo log\n  vehicle_info?: {\n    known_issues_summary?: string;\n    reliability_by_year?: Array\u003c{ year: number; breakdowns_per_1000: number; rating: string; rating_score: number; generation_name?: string }\u003e;\n    generations?: Array\u003c{ name: string; year_from: number; year_to?: number }\u003e;\n    image_url?: string;\n    adac_page_url?: string;\n  };\n  issue_patterns: Array\u003c{\n    pattern_name: string;\n    symptoms: string[];\n    root_cause: string;\n    solution: string;\n    severity?: \"low\"|\"medium\"|\"high\";\n    affected_years?: string;\n  }\u003e;\n  found: boolean;                         // true when vehicle_info is populated\n  elapsed_ms: number;\n}\n```\n\n```bash\ncurl -X POST $BASE/vehicle/lookup \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"Golf\",\"year\":2019}'\n```\n\n### `POST /image/analyze`\n\nAccepts either a URL/data-URI (form field) or a file upload (multipart).\n\n**Request (multipart or urlencoded)**\n- `image_url` *(string, optional)* — `http(s)://…` or `data:image/jpeg;base64,…`\n- `image` *(file, optional)* — alternative to `image_url`\n- `context` *(string, optional)* — e.g. `\"dashboard warning lights\"`\n\n**Response**\n```ts\n{\n  request_id: string;\n  observations: string[];\n  possible_findings: string[];\n  warning_lights_detected: string[];      // e.g. [\"engine_warning\",\"oil_pressure\"]\n  damage_detected: boolean;\n  limitations: string[];\n  confidence: number;                     // 0–1\n  raw_description?: string;\n  vehicle_detected: boolean;\n  vehicle_count: number;\n  image_quality: \"good\"|\"poor\"|\"unusable\";\n  needs_clarification: boolean;\n  clarification_questions: string[];\n  detected_make?: string;                 // from HF dima806 classifier\n  detected_model?: string;\n  vehicle_boxes: Array\u003c{ label: string; x1: number; y1: number; x2: number; y2: number; confidence: number }\u003e;  // 0–1 normalized\n  image_rotation_deg: 0|90|180|270;\n  adac_summary?: string;                  // auto-fetched when make/model resolved\n  adac_issue_patterns?: object[];\n  elapsed_ms: number;\n}\n```\n\n```bash\n# File upload\ncurl -X POST $BASE/image/analyze -F \"image=@dashboard.jpg\"\n\n# URL\ncurl -X POST $BASE/image/analyze \\\n  -F \"image_url=https://example.com/car.jpg\" \\\n  -F \"context=front bumper damage\"\n```\n\n### `GET /debug/graph`\n\nReturns the compiled LangGraph structure for visualization.\n\n```ts\n{ nodes: string[]; edges: Array\u003c{ from: string; to: string; condition?: string }\u003e; entry_point?: string }\n```\n\n---\n\n## Frontend integration\n\nMinimal TypeScript client:\n\n```ts\nconst BASE = \"https://8000-bfa16ffa-8c1c-45c1-91b5-ae17fbd72b23.daytonaproxy01.eu\";\n\nasync function chat(query: string, image_url?: string) {\n  const r = await fetch(`${BASE}/chat`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ query, image_url }),\n  });\n  return r.json();\n}\n\nasync function lookup(model: string, year?: number, make?: string) {\n  const r = await fetch(`${BASE}/vehicle/lookup`, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ model, year, make }),\n  });\n  return r.json();\n}\n\nasync function analyzeImage(file: File, context?: string) {\n  const form = new FormData();\n  form.append(\"image\", file);\n  if (context) form.append(\"context\", context);\n  const r = await fetch(`${BASE}/image/analyze`, { method: \"POST\", body: form });\n  return r.json();\n}\n```\n\nTypical UI wiring:\n\n- **Chat box + optional drop zone** → `POST /chat`. Render `answer`, show `sources` as pills, turn `clarification_questions` into buttons that re-submit the selected clarification.\n- **Standalone lookup widget** → `POST /vehicle/lookup`. Render `vehicle_info.reliability_by_year` as a bar chart; list `issue_patterns` as expandable cards.\n- **Image analysis panel** → `POST /image/analyze`. Overlay `vehicle_boxes` on the uploaded image; render `warning_lights_detected` as badges.\n\nError handling: all endpoints return HTTP 200 on degraded outcomes — check `confidence`, `found`, `limitations`, and `uncertainty_notes` in the body. HTTP 4xx/5xx indicate protocol-level failures.\n\nThe repo ships with a reference frontend at `frontend/index.html` (~1400 lines of vanilla HTML/CSS/JS, no build step). The FastAPI app mounts it at `/ui/`, so the live demo URL and the API URL are the same origin.\n\n---\n\n## Quick start (local)\n\n```bash\ngit clone https://github.com/kaiser-data/carlover\ncd carlover\npython3 -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -e \".[dev]\"\ncp .env.example .env                      # fill in API keys\nuvicorn app.main:app --reload --port 8000\n```\n\nOpen http://localhost:8000/ui/ in a browser.\n\nRequires Python ≥ 3.12.\n\n---\n\n## Deployment\n\n### Daytona sandbox (backend)\n\n```bash\npip install daytona-sdk\npython3 scripts/deploy_daytona.py             # create + upload + start\npython3 scripts/deploy_daytona.py --status    # show state + preview URL\npython3 scripts/deploy_daytona.py --stop      # stop the current sandbox\n```\n\nThe script reads `.env`, creates a **public** Daytona sandbox with `auto_stop_interval=0`, uploads the project tree, `pip install`s runtime deps, starts `uvicorn` in a background session, and writes the sandbox ID to `.daytona_sandbox_id`. The preview URL printed at the end is the UUID-form public URL (e.g. `https://8000-\u003cuuid\u003e.daytonaproxy01.eu`); the signed short URL is also printed but not needed for public access.\n\n### Supabase Edge Function (ADAC proxy)\n\nRequired when `ADAC_PROVIDER=real` and the backend is hosted on Daytona — the sandbox can't reach `adac.de` directly.\n\n```bash\nbrew install supabase/tap/supabase        # or equivalent\nexport SUPABASE_ACCESS_TOKEN=sbp_...      # from supabase.com/dashboard/account/tokens\nsupabase functions deploy adac-proxy \\\n  --project-ref \u003cyour-project-ref\u003e --no-verify-jwt\n```\n\nThe function is ~60 lines of Deno (`supabase/functions/adac-proxy/index.ts`), rejects any host other than `www.adac.de`, and adds a 1-hour public cache header. Supabase's free tier allows 500K invocations/month.\n\n### Docker (alternative)\n\n```bash\ndocker compose up --build\n```\n\nServes on `localhost:8000` with the same env-var contract.\n\n---\n\n## Environment variables\n\n| Variable | Required? | Notes |\n|---|---|---|\n| `LLM_PROVIDER` | yes | `groq` (default) or `featherless` — both OpenAI-compatible |\n| `GROQ_API_KEY` | when `LLM_PROVIDER=groq` | |\n| `FEATHERLESS_API_KEY` | when `LLM_PROVIDER=featherless` | |\n| `GROQ_MODEL_*` / `FEATHERLESS_MODEL_*` | no | Per-task overrides: `ORCHESTRATOR`, `REASONING`, `VISION`, `RESPONSE` |\n| `HUGGINGFACE_API_KEY` | no | Enables HF DETR + dima806 classifier. Empty = VLM-only. |\n| `HF_DETECTION_MODEL` / `HF_CLASSIFICATION_MODEL` | no | Override default HF model IDs |\n| `SUPABASE_URL` / `SUPABASE_KEY` | yes | Service-role JWT. Also used by the ADAC edge-function proxy. |\n| `SUPABASE_ACCESS_TOKEN` | deploy-only | `sbp_…` token, used solely by `supabase functions deploy` |\n| `ADAC_PROVIDER` | yes | `real` (scrape) or `mock` (bundled fixtures) |\n| `SCRAPER_API_KEY` | no | Optional middle fallback between Supabase proxy and direct fetch |\n| `DAYTONA_API_KEY` | yes | Needed by the sandbox agent and the deploy script |\n| `DAYTONA_API_URL` | no | Defaults to `https://app.daytona.io/api` |\n| `DEBUG` | no | `true` → include per-node timing in `/chat` responses |\n| `LOG_LEVEL` | no | `INFO` default |\n\n---\n\n## Tests\n\n```bash\npytest                       # full suite\npytest tests/test_image_detection.py -v  # single file\n```\n\nSuite covers:\n- `test_health.py` — liveness + graph compile smoke\n- `test_chat_flow.py` — end-to-end chat with mocked LLM\n- `test_graph_routing.py` — clarification vs. run_subagents branch\n- `test_image_detection.py` — vehicle count, multi-car, warning lights, rotation, no-vehicle, **HF hybrid path**, brand/model splitter\n- `test_image_flow.py` — `/image/analyze` request/response\n- `test_adac_agent.py` — real-provider parsing fixtures + keyword filtering\n- `test_supabase_agent.py` — service-history agent against a mocked client\n\nNo network calls in CI — all external providers are mocked.\n\n---\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaiser-data%2Fcarlover","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkaiser-data%2Fcarlover","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaiser-data%2Fcarlover/lists"}