All projects

feedo

Plain-Persian bug reports in, structured AI drafts out: vision-LLM drafting, duplicate detection and truncated-JSON recovery.

Feedo: reports dashboard and mobile intake form (demo data)

Plain-Persian bug reports in, structured AI drafts out.

Farsi bug-report intake: a non-technical user describes a problem in plain Persian (with screenshots), a Gemini vision model turns it into a structured, classified draft, and QA and managers work it through a simple lifecycle.

Highlights

  • One multimodal call turns free-form Farsi text plus screenshots into a title, description, repro steps, severity, type and module.
  • The model never picks IDs. Module and duplicate verdicts are validated against real catalog rows and real candidate reports.
  • Duplicate detection grounded in the same-module backlog, stored as a duplicate_of link.
  • Truncated-JSON salvage keeps every completed field when a token-heavy Persian response is cut off.
  • Human in the loop: AI triage suggests approve/reject on the Manager’s approvals page; people decide.
  • RTL-first UI: Vazirmatn, Persian digits, Jalali dates, responsive down to phone width.

Screenshots

All screenshots show the real app running locally with fictional demo data (app.seed.seed_demo); the AI fields are seeded, not produced by a live model call.

Reports dashboardAI draft review
Reports dashboard: status counters, filters, severity and type at a glance (demo data)AI draft review: the reporter’s raw text next to the editable structured draft and AI verdict (demo data)
Approvals with AI triageIntake form
Approvals: AI category, recommendation and reasoning for each new report (demo data)Intake form: describe the problem in plain Persian, pick a module and its sections, attach screenshots (demo data)

Mobile intake form (demo data)

Why it’s interesting

  • Free-form Farsi + screenshots in, structured draft out. One multimodal call (app/services/llm.py, LLMService.organize) reads the raw text and the attached images and returns a Persian title, description, steps, severity, report type, and module/section.
  • The model is never trusted with identifiers. It picks a module by key from the real catalog and the service maps that to a row id (or None). A duplicate verdict is only accepted if the returned code matches one of the candidate reports shown in the prompt.
  • Duplicate detection grounded in existing reports. Candidate reports from the same module (code, status, snippet, latest comments) are put in the prompt and capped in size; the model may only flag a duplicate against one of them, and the link is stored as duplicate_of.
  • Truncated-JSON recovery. Persian is token-heavy and thinking models can hit finish_reason=length mid-object. _tolerant_json strips code fences and surrounding prose, and _salvage_truncated recovers every completed top-level key/value pair instead of discarding the response.
  • Human stays in the loop. The LLM only suggests; nothing it returns is written without a person confirming. Roles (Reporter / QA / Manager) live in the database, not in the Keycloak token.

Architecture

flowchart LR
  U[Reporter / QA / Manager] --> FE[Next.js 16 frontend<br/>RTL, Farsi, Jalali dates]
  FE -->|/api/proxy injects Bearer| API[FastAPI backend]
  KC[Keycloak OIDC] --- FE
  KC --- API
  API --> PG[(PostgreSQL)]
  API -->|text + screenshots| LLM[Gemini via OpenRouter]
  API -.->|critical bugs, optional| BOT[chat-bot webhook sidecar]

The frontend never calls the API directly: a same-origin proxy route attaches the access token server-side. The backend has routers, services (llm, reports, notifications, report_io, attachments), async SQLAlchemy models and Alembic migrations. Report lifecycle: submitted -> approved (Manager) -> in_progress (QA) -> done, or rejected.

Tech stack

  • Backend: Python 3.12, FastAPI, async SQLAlchemy 2, Alembic, Pydantic v2, PostgreSQL (pg_trgm search indexes), uv
  • Frontend: Next.js 16 App Router, React 19, TypeScript, Tailwind v4, TanStack Query, NextAuth (Keycloak), Radix UI, Jalali calendar
  • LLM: OpenAI-compatible client pointed at OpenRouter (default google/gemini-3.6-flash, native vision)

Key techniques

  • Multimodal prompting with base64 screenshots: app/services/llm.py, app/services/reports.py
  • Tolerant parsing and truncated-JSON salvage: _tolerant_json, _salvage_truncated in app/services/llm.py
  • Prompts that ask for JSON plus an appended key/type shape hint instead of response_format (some Gemini models on OpenRouter reject it)
  • Duplicate and classification verdicts validated against real rows: LLMService.organize
  • Sha256 de-duplication of re-uploaded attachments: app/services/attachments.py
  • Bulk report import/export with two-pass duplicate_of re-linking: app/services/report_io.py
  • Role-based permissions read from the DB: app/core/permissions.py

Getting started

Requires Python 3.12 + uv, Node 20+, PostgreSQL, and a Keycloak realm (or ENV=dev with DEV_AUTH=true locally).

# backend
cd backend
cp .env.example .env          # set DATABASE_URL, KEYCLOAK_*, LLM_API_KEY, LLM_ENABLED=true
uv sync
uv run alembic upgrade head
uv run python -m app.seed.seed_catalog
uv run uvicorn app.main:app --reload --port 8000

# frontend
cd ../frontend
cp .env.example .env.local
npm install
npm run dev                   # http://localhost:3000

The seeded module/section catalog is a small sample; replace it with your product’s own.

Demo mode (no Postgres, Keycloak or LLM key)

A local SQLite database with fictional reports, and the dev-only auth bypass (ENV=dev + DEV_AUTH=true, signed in as a Manager):

# backend
cd backend
export DATABASE_URL=sqlite+aiosqlite:///./demo.db ENV=dev DEV_AUTH=true
uv run python -m app.seed.seed_demo          # drops and recreates demo.db
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000

# frontend (another shell)
cd frontend
NEXTAUTH_SECRET=local-dev-only BACKEND_URL=http://127.0.0.1:8000 \
DEV_AUTH=true NEXT_PUBLIC_DEV_AUTH=true npm run dev

The LLM stays off in demo mode, so “re-draft with AI” is unavailable; the drafts shown are seeded. scripts/capture-screenshots.js regenerates docs/images/ from a running demo (needs Playwright).

Tests

cd backend
LLM_ENABLED=0 uv run pytest -m "not integration"   # 91 tests in 7 modules, in-memory SQLite, LLM stubbed
uv run ruff check . && uv run ty check
cd ../frontend && npx tsc --noEmit && npx eslint

The last backend run passed (91 tests). The frontend has no test suite, only type-check and lint.

Notes

The critical-bug notifier posts to an external chat-bot sidecar that is not part of this repo; it is off by default (NOTIFY_ENABLED=false). Users and roles are Keycloak-provisioned; there is no local signup.

License

MIT, see LICENSE.


Built by Sepehr Radmard · LinkedIn · GitHub · more projects on my profile