diff --git a/README.md b/README.md index 2346907..d87387c 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,1404 @@ -# Riksdagen sökplattform +# Riksdagen Sökplattform – Teknisk Översikt -En moderniserad version av den tidigare Streamlit-sidan. Backenden kör FastAPI + PostgreSQL, frontenden är byggd i React/Vite med statisk distribution via nginx. +En plattform för att utforska riksdagsdebatter och metadata. Backenden bygger på FastAPI, ArangoDB (med ArangoSearch), samt ChromaDB för vektorsök i chatten. Frontenden är en React/Vite-applikation skriven i TypeScript. LLM-funktionalitet kopplas in via `_llm`-paketet. -## Struktur +--- -- `backend/` – FastAPI-app, SQLAlchemy-tjänster, Pydantic-scheman. -- `frontend/` – React-kod, komponenter, Vite-konfiguration, CSS. -- `info.py` – Partifärger, texttexter, konstanter som återanvänds i backend. -- `config.py` – Databasuppkoppling (används av backend). -- `scripts/` – Plats för hjälpskript (t.ex. vector-inmatning). -- `Makefile` – Snabbkommandon (bygga frontend, starta backend, ladda om nginx). +## 📋 Innehållsförteckning -## Vanliga arbetsflöden +1. [Systemöversikt](#systemöversikt) +2. [Projektstruktur](#projektstruktur) +3. [Backend-arkitektur](#backend-arkitektur) +4. [Frontend-arkitektur](#frontend-arkitektur) +5. [Dataflöden](#dataflöden) +6. [Viktiga Python- och TypeScript-komponenter](#viktiga-python--och-typescript-komponenter) +7. [ArangoDB och ArangoSearch](#arangodb-och-arangosearch) +8. [ChromaDB för vektorsök](#chromadb-för-vektorsök) +9. [LLM-integration](#llm-integration) +10. [Utvecklingsflöde](#utvecklingsflöde) +11. [Felsökning och tips](#felsökning-och-tips) -| Åtgärd | Kommando | -| ------ | -------- | -| Starta backend för utveckling | `uvicorn backend.app:app --reload --port 8000` | -| Bygga frontend (från projektroten) | `make frontend` *(kör `npm install` och `npm run build` i `frontend/`)* | -| Endast frontendbuild | `cd frontend && npm run build` | -| Ladda om nginx efter ny build | `sudo systemctl reload nginx` | -| Starta om backend efter Python-ändring | Avsluta befintlig uvicorn-process och kör kommandot ovan igen | +--- -> OBS: `npm run build` måste köras inifrån `frontend/` (eller via `make frontend`) eftersom `package.json` ligger där. +## 🏗️ Systemöversikt -## Frontend +``` +┌─────────────┐ HTTP ┌──────────────┐ Python ┌───────────────┐ +│ Browser │ ─────────▶ │ FastAPI │ ───────────▶ │ ArangoDB │ +│ (React) │ ◀───────── │ backend │ ◀─────────── │ (ArangoSearch) │ +└─────────────┘ └──────┬───────┘ └───────────────┘ + │ + │ embeddings / metadata + ▼ + ┌──────────────┐ + │ ChromaDB │ + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ _llm │ (OpenAI API-kompatibel klient) + └──────────────┘ +``` + +- **ArangoDB**: primär databas. Sök görs via ArangoSearch-vyer i stället för SQL. +- **ChromaDB**: håller embeddings för chatten och mer avancerad semantisk sökning. +- **LLM**: genererar chattsvar med hjälp av `_llm.LLM` (kopplad till t.ex. vLLM/OpenAI API). + +--- + +## 📁 Projektstruktur + +``` +riksdagen/ +├── arango_client.py # Initierar Arango-klienten (används av backend & scripts) +├── backend/ +│ ├── app.py # FastAPI-app, routerregistrering +│ ├── search.py # SearchService med ArangoSearch-frågor +│ ├── chat.py # Chat-endpoints (LLM + Chroma) +│ ├── vector.py # Hjälpfunktioner för vektorsök (omfattar Chroma-anrop) +│ └── ... # Övriga routrar, scheman och konfigurationer +├── frontend/ +│ ├── src/ +│ │ ├── App.tsx # Inträde, växlar mellan sök/chat-lägen +│ │ ├── api.ts # Axios-klient mot `/api/*` +│ │ ├── types.ts # TypeScript-typer för API-svar +│ │ ├── components/ # UI-komponenter (SearchPanel, ChatPanel m.fl.) +│ │ └── styles.css # Globala stilar +│ └── vite.config.ts +├── scripts/ +│ ├── ingest_arguments.py # Exempel: läser in data till ArangoDB +│ └── build_embeddings.py # Skapar embeddings och för in i Chroma +├── _arango/ +│ ├── _arango.py # Wrapper kring python-arango (förbättrad initiering) +│ ├── queries/ # Samling av AQL/ArangoSearch-frågor +│ └── helpers.py # Hjälpfunktioner för AQL-byggande +├── _chromadb/ +│ ├── client.py # Instansierar ChromaDB-klient +│ ├── collections.py # Hanterar samlingar (persistens, insättning, sök) +│ └── embeddings.py # Hjälp för integrering mot embeddingsgenerator +├── _llm/ +│ └── llm.py # LLM-wrapper (OpenAI API-kompatibelt gränssnitt) +└── README.md +``` + +> **Tips:** Utforska `_arango`, `_chromadb` och `_llm` för att förstå hur databaskopplingar och LLM-anrop är kapslade. + +--- + +## 🔧 Backend-arkitektur + +### `backend/app.py` + +- Skapar FastAPI-instansen och exponerar endpoints under `/api`. +- Registrerar t.ex. `search_router`, `chat_router`, `meta_router`. +- Konfigurerar CORS så att frontenden kan nå backenden under utveckling och produktion. + +### `backend/search.py` + +- Innehåller klassen **`SearchService`**. +- `SearchService` använder `arango_client` och `_arango`-hjälpare för att köra ArangoSearch-frågor. +- Vanliga metoder (namn kan skilja något, läs filen för exakt signatur): + - `search(...)`: bygger en ArangoSearch AQL-query med filter (parti, år, talare) samt scoring via `BM25()` eller `TFIDF()`. + - `get_metadata(...)`: hämtar distinkta värden till frontendens filter. + - `get_document_by_id(...)`: används för detaljerad visning/snippets. +- **Viktigt:** ingen SQL används. AQL byggs dynamiskt och exekveras mot en ArangoSearch-vy (se `_arango/queries`). + +### `backend/chat.py` + +- Hanterar endpointen `POST /api/chat`. +- Steg i chatthanteringen: + 1. Plockar senaste användarfrågan. + 2. Anropar ChromaDB (via `_chromadb`) för semantisk återhämtning. + 3. Kompletterar kontext med Arango-sökresultat om relevant. + 4. Bygger prompt och anropar `_llm.LLM`. + 5. Returnerar AI-svaret plus metadata (t.ex. vilka dokument som användes). +- Har ofta även `POST /api/vector-search` som låter frontenden testa Chroma-resultat separat. + +### `backend/vector.py` (eller motsvarande modul) -- Inträdet är `frontend/src/App.tsx`. -- Sökpanel, statistik och tabell ligger i `frontend/src/components/`. -- Globala stilar i `frontend/src/styles.css`. -- För att ändra startsidetexten, justera rubriken/tagline i `App.tsx`. -- Partifärger kommer från backend-responsen; nya partier kan läggas till i `info.py`. +- Innehåller hjälpfunktioner för att: + - Normalisera text. + - Skicka query till ChromaDB och hämta top-k embeddings-matchningar. + - Eventuellt uppdatera/återskapa Chroma-samlingar. -Efter frontend-ändringar: -1. `cd frontend` -2. `npm run build` -3. `sudo systemctl reload nginx` +--- -## Backend +## ⚛️ Frontend-arkitektur -- FastAPI-app: `backend/app.py`. -- Databasaccess: `backend/services/search.py` (SQL och snippets). -- Pydantic-modeller: `backend/schemas.py`. -- API-endpoints: - - `POST /api/search` - - `GET /api/meta` - - `POST /api/feedback` - - `POST /api/chat` *(förberett för LLM/pgvector)* +- **`App.tsx`**: växlar mellan sök- och chat-läge (t.ex. via `const [mode, setMode] = useState<'search' | 'chat'>('search')`). +- **`api.ts`**: axios-klient som exporterar: + - `searchTalks(params)` → `POST /api/search`. + - `chat(messages, strategy)` → `POST /api/chat`. + - `vectorSearch(query)` → `POST /api/vector-search`. + - `getMeta()` → `GET /api/meta`. +- **`components/SearchPanel.tsx`**: + - Kontrollerade inputs för fritext, år, parti m.m. + - Använder `react-query` (`useMutation/useQuery`) för att trigga backend-anrop. + - Visar resultat i `ResultsTable.tsx`. +- **`components/ChatPanel.tsx`**: + - Håller konversation i state (lista av `{ role, content }`). + - Skickar hela historiken till chat-endpointen. + - Renderar streamen av svar. +- **`types.ts`**: + - Speglar backendens Pydantic-scheman med TypeScript-interfaces (`SearchRequest`, `SearchResponse`, `TalkResult`, `ChatRequest`, etc.). + - Bra översättning mellan Python-modeller och TypeScript. -Vid backendkodförändring: -1. Stoppa uvicorn-processen (`lsof -i :8000` + `kill ` vid behov). -2. Starta om med `uvicorn backend.app:app --reload --port 8000`. +--- -## Backend-översikt +## 🔄 Dataflöden + +### Sökning via ArangoSearch + +1. **Frontend** samlar filtervärden (t.ex. `query="skola"`, `party="S"`, `year=2023`). +2. `api.searchTalks` gör POST till `/api/search`. +3. **FastAPI** tar emot request → skapar `SearchService`. +4. `SearchService.search`: + - Bygger AQL med `SEARCH`-klasuler mot en ArangoSearch-vy (exempel: `SEARCH ANALYZER(doc.anforande_text, "text_sv") LIKE "%skola%"`). + - Använder `SORT BM25(doc)` eller `SORT doc.datum` beroende på begärda sorteringsparametrar. + - Begränsar resultat (`LIMIT offset, limit`) och returnerar träffar + total count. +5. Backend serialiserar dokumenten till JSON (inklusive metadata som färger, snippet, etc.). +6. **Frontend** renderar resultat och uppdaterar paginering/statistikpaneler. + +### Chatt med LLM + +1. Användaren skriver fråga i chat-panelen. +2. `api.chat` skickar en lista av meddelanden (`[{ role: 'user', content: ... }, ...]`). +3. `backend/chat.py`: + - Tar ut senaste användarfrågan. + - `vector_search(...)` i samma modul eller via `_chromadb` hämtar semantiskt relevanta avsnitt. + - Kan komplettera svar med `SearchService`-resultat för att säkerställa faktabas. + - Lägger samman kontext och anropar `_llm.LLM`. +4. AI-svaret skickas tillbaka till frontenden och läggs på konversationen. + +--- + +## 🧩 Viktiga Python- och TypeScript-komponenter | Fil | Funktion | -| --- | -------- | -| `backend/app.py` | Skapar FastAPI-instansen. Inkludera `from backend.routes.chat import router as chat_router` och `app.include_router(chat_router)` här. | -| `backend/routes/chat.py` | Endpoints för `/api/chat` och `/api/vector-search`. | -| `backend/services/embedding.py` | Funktioner för chunkning och hämtning av embeddings via Ollama. | -| `backend/services/vector_store.py` | Skapar `talk_embeddings`-tabellen och kör likhetsfrågor med pgvector. | -| `scripts/build_embeddings.py` | Kommandoradsverktyg som fyller tabellen med embeddings. | +|-----|----------| +| `arango_client.py` | Central Arango-klient. Återanvänds i backend och scripts. | +| `_arango/_arango.py` | Wrapper som läser miljövariabler, öppnar anslutningar och ger helpers för views/collections. | +| `_arango/queries/*` | Samlade AQL-snippets för sökning, metadata, detaljer. Läs dessa för att förstå exakta fält och filtreringslogik. | +| `_chromadb/client.py` | Skapar en Chroma-klient (kan vara persistent eller in-memory beroende på config). | +| `_chromadb/collections.py` | Hjälpfunktioner för att skapa/hämta Chroma-samlingar och göra `query`/`add`. | +| `_llm/llm.py` | LLM-wrapper. `LLM(model='vllm')` returnerar en klient med `.chat(messages=...)`. | +| `backend/search.py` | `SearchService` med AQL/ArangoSearch-hantering och responsformatering. | +| `backend/chat.py` | Chat-endpoints, orchestrerar Chroma + LLM + (valfritt) Arango-sök. | +| `frontend/src/api.ts` | Samlar alla HTTP-anrop till backend. Lätt att mocka i tester. | +| `frontend/src/components/*` | React-komponenter. Kommentera gärna i koden var data kommer ifrån och vad som skickas tillbaka till backend. | + +--- + +## 🗄️ ArangoDB och ArangoSearch + +- **Data** ligger i ArangoDB-kollektioner, ofta `talks`, `speakers`, `debates`, etc. +- **Views**: en ArangoSearch-vy (t.ex. `talks_view`) indexerar textfält med svenska analyser (`text_sv`). Detta gör att `SEARCH`-frågor kan använda ranking (BM25/TFIDF) och filtrera på fält. +- **AQL-byggstenar** (se `_arango/queries`): + - `FOR doc IN talks_view` + - `SEARCH ANALYZER(doc.anforande_text, "text_sv") LIKE "%..."%` + - `FILTER doc.party == @party` + - `SORT BM25(doc) DESC` + - `LIMIT @offset, @limit` +- **Metadata**: Distinkta listor (ex. partier, år) hämtas via AQL `COLLECT`. + +> **Ny i ArangoSearch?** Läs igenom `_arango/queries` och Arangos officiella exempel. Notera att ArangoSearch skiljer sig från vanliga AQL-index genom att all textindexering sker i vyn, inte kollektionen. + +--- + +## 🧠 ChromaDB för vektorsök + +- `_chromadb/client.py` konfigurerar anslutningen (lokal fil, DuckDB + Parquet som standard). +- Embeddings genereras i `scripts/build_embeddings.py`: + 1. Hämtar tal från ArangoDB med `arango_client`. + 2. Delar upp text i chunkar (se `_chromadb/embeddings.py` för heuristiken). + 3. Kör embeddingsgenerator (vanligen via `_llm` eller egen modul). + 4. Lagrar chunkar + metadata (`talk_id`, `speaker`, `party`, `source`) i Chroma-samlingen. + +- Vid chat: + - `vector_search` skickar användarfrågan som embedding till Chroma (`collection.query(query_texts=[...], n_results=top_k)`). + - Resultatet innehåller `ids`, `documents`, `metadatas`, `distances` som matas in i prompten. + +--- + +## 🤖 LLM-integration + +- `_llm/llm.py` är huvudgränssnittet. Grundmönster: + + ```python + from _llm import LLM + + def generate_answer(prompt: str) -> str: + llm = LLM(model="vllm") + messages = [ + {"role": "system", "content": "Du är en assistent som förklarar riksdagsdebatter sakligt."}, + {"role": "user", "content": prompt}, + ] + response = llm.chat(messages=messages) + return response + ``` + +- Miljövariabler (`CHAT_MODEL`, `LLM_BASE_URL`, `LLM_API_KEY`) styr målmodell och endpoint. +- Chat-endpointen täcker redan vanlig användning. För experiment: återanvänd samma mönster men bygg tydlig prompt av kontexten du hämtar. + +> Undvik onödiga `try/except`. Fel bubbla upp så att du ser den faktiska orsaken (t.ex. nätverksproblem, fel API-nyckel). + +--- + +## 🛠️ Utvecklingsflöde + +| Steg | Befallning | Kommentar | +|------|------------|-----------| +| Starta backend i utvecklingsläge | `uvicorn backend.app:app --reload --port 8000` | Körs i projektroten. Automatiskt reload vid filändring. | +| Starta frontend i dev-läge | `cd frontend && npm run dev` | Öppna http://localhost:5173 | +| Bygg frontend för produktion | `make frontend` | Skapar `frontend/dist`. | +| Ladda om nginx (om du serverar statiskt) | `sudo systemctl reload nginx` | Efter ny frontend-build. | +| Kör embeddings-script | `python -m scripts.build_embeddings --limit 500` | Uppdaterar Chroma-samlingen. | +| Kör anpassade Arango-script | `python -m scripts.ingest_arguments` | Exempel, kontrollera scriptens docstrings. | + +--- + +## 🧪 Felsökning och tips + +- **Arango-anslutning**: kör `python arango_client.py` för att testa att lösenord och URL stämmer. +- **Chroma-data**: öppna `_chromadb/client.py` och kontrollera `persist_directory`. Rensa katalogen om du behöver resetta. +- **Chat-svar saknar fakta**: kontrollera att `build_embeddings.py` har körts nyligen och att Chroma-samlingen innehåller rätt metadata. +- **React/TypeScript**: + - Använd `useQuery` (för GET) och `useMutation` (för POST). + - Håll komponenter små och bryt ut delkomponenter vid behov (t.ex. `FilterControls`, `ChatMessageList`). + - Typsätt props, state och API-svar med `types.ts` för bättre editorstöd. +- **Python-stil**: + - Lägg till type hints på funktioner (ex: `def search(self, params: SearchRequest) -> SearchResponse:`). + - Skriv docstrings som beskriver syfte, parametrar och returvärden. + - Undvik generella `try/except`; analysera felen vid körning och fixa orsaken. + +--- + +## 🤔 Nästa steg + +1. **Kommentera kod**: lägg till docstrings i `SearchService` och chat-hjälpare. +2. **Testa ArangoQueries**: skapa enhetstester som kör små AQL-scenarier med testdata. +3. **Frontend-guidning**: överväg inline-kommentarer i React-komponenterna för att förklara state-hantering. +4. **Observabilitet**: logga vilka ArangoSearch-queries som körs vid olika filterkombinationer. + +--- + +## 📎 Referenser + +- [ArangoDB Documentation – ArangoSearch](https://www.arangodb.com/docs/stable/arangosearch.html) +- [ChromaDB Python API](https://docs.trychroma.com/) +- [FastAPI – Depends och Pydantic](https://fastapi.tiangolo.com/) +- [React + TypeScript Cheatsheets](https://react-typescript-cheatsheet.netlify.app/) +- `batch_embed()` – Processar flera texter samtidigt + +**Exempel**: +```python +def get_embedding(text: str, model: str = "embeddinggemma") -> List[float]: + """ + Genererar en embedding-vektor för given text. + + Args: + text: Texten att embedda + model: Ollama-modell (default: embeddinggemma) + + Returns: + Lista med floats (3072 dimensioner för embeddinggemma) + """ + # Anropar http://192.168.1.11:11434/api/embeddings + # Returnerar vektor +``` + +#### `backend/services/vector_store.py` + +Hanterar pgvector-operationer: +- `create_embeddings_table()` – Skapar `talk_embeddings`-tabell +- `store_embedding()` – Sparar embedding i databas +- `query_similar_talks()` – Semantisk sökning + +--- + +### Database: `backend/database.py` + +**Viktigt**: Denna fil skapar databaskopplingen. + +```python +# Skapar engine (kopplingen till PostgreSQL) +engine = create_engine(DATABASE_URL) + +# Aktiverar pgvector-extension +with engine.connect() as conn: + conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) + +# Session factory – används för att skapa databas-sessioner +SessionLocal = sessionmaker(bind=engine) + +# Dependency för FastAPI – ger varje endpoint en databas-session +def get_db(): + db = SessionLocal() + try: + yield db # Ger sessionen till endpointen + finally: + db.close() # Stänger sessionen efter request +``` + +**Hur används det**: +```python +@router.post("/api/search") +def search_endpoint(request: SearchRequest, db: Session = Depends(get_db)): + # db är nu en aktiv databas-session + results = search_talks(db, ...) +``` + +--- + +## ⚛️ Frontend-arkitektur + +### Huvudfil: `frontend/src/App.tsx` + +Detta är root-komponenten som renderas i `index.html`. + +```typescript +function App() { + const [mode, setMode] = useState<'search' | 'chat' | 'vector'>('search'); + + // mode styr vilket läge användaren är i: + // - 'search': Vanlig nyckelordssökning + // - 'chat': AI-chattläge + // - 'vector': Semantisk vektorsökning + + return ( +
+ {/* Knappar för att växla läge */} + + + + {/* Visar olika komponenter baserat på mode */} + {mode === 'search' && } + {mode === 'chat' && } + {mode === 'vector' && } +
+ ); +} +``` + +**Viktiga React-koncept**: +- `useState`: Skapar state-variabel som kan ändras. När `mode` ändras, re-renderar React komponenten. +- `{ }`: JavaScript-uttryck inuti JSX (React's HTML-liknande syntax) +- `&&`: Villkorlig rendering – `condition && ` renderar bara om condition är true + +--- + +### API-klient: `frontend/src/api.ts` + +Detta är hur frontend pratar med backend. + +```typescript +import axios from 'axios'; + +// Base URL – i produktion går detta via nginx reverse proxy +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; + +// axios-instans med konfigurerad base URL +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { 'Content-Type': 'application/json' } +}); + +// Funktion för att söka +export async function searchTalks(params: SearchParams): Promise { + const response = await api.post('/search', params); + return response.data; // Backend returnerar JSON +} -`backend/database.py` initierar pgvector-extensionen automatiskt vid anslutning. +// Funktion för att chatta +export async function chat(messages: ChatMessage[]): Promise { + const response = await api.post('/chat', { messages }); + return response.data; +} +``` + +**TypeScript-koncept**: +- `async/await`: Hanterar asynkrona operationer (API-anrop) +- `Promise`: Representerar ett framtida värde av typ T +- `interface`: Definierar shape av objekt (se `types.ts`) + +--- + +### Komponenter + +#### `frontend/src/components/SearchPanel.tsx` -## Konfiguration +Sökformulär med filter. -Ställ in miljövariabler vid behov: +**State-hantering**: +```typescript +const [query, setQuery] = useState(''); // Fritextsök +const [party, setParty] = useState(''); // Valt parti +const [year, setYear] = useState(''); // Valt år +// ...fler filter +// react-query mutation för att söka +const searchMutation = useMutation({ + mutationFn: searchTalks, + onSuccess: (data) => { + // data innehåller SearchResponse från backend + // Skickas till ResultsTable via props + } +}); + +// När användaren klickar "Sök" +const handleSearch = () => { + searchMutation.mutate({ + query, + party, + year, + // ...andra filter + }); +}; ``` -export OLLAMA_BASE_URL=http://192.168.1.11:11434 -export EMBEDDING_MODEL=embeddinggemma -export EMBEDDING_DIMENSION=3072 -export LLM_BASE_URL=http://localhost:8000/v1 -export LLM_API_KEY=din-nyckel-eller-placeholder -export CHAT_MODEL=gpt-3.5-turbo + +**React Query (`useMutation`)**: +- Hanterar API-anrop automatiskt +- Ger tillgång till `isLoading`, `error`, `data` +- Cachas resultat för snabbare återanvändning + +**JSX-exempel**: +```tsx + setQuery(e.target.value)} + placeholder="Sök efter innehåll..." +/> ``` -## Bygga embeddings +**Hur det fungerar**: +1. `value={query}` – Input-fältet visar värdet av `query` state +2. `onChange` – När användaren skriver, uppdateras `query` med `setQuery()` +3. Detta är "controlled component" pattern i React + +--- -1. Starta Ollama på `OLLAMA_BASE_URL`. -2. Kör scriptet: +#### `frontend/src/components/ResultsTable.tsx` +Visar sökresultat i tabell. + +```typescript +interface ResultsTableProps { + results: TalkResult[]; // Array av anföranden från backend + total: int; // Totalt antal träffar + isLoading: boolean; // Om data laddas +} + +function ResultsTable({ results, total, isLoading }: ResultsTableProps) { + if (isLoading) return
Laddar...
; + + if (results.length === 0) return
Inga resultat
; + + return ( + + + + + + + + + + + {results.map((talk) => ( + + + + + + + ))} + +
DatumTalarePartiInnehåll
{talk.datum}{talk.talare} + {talk.parti} + {talk.snippet}
+ ); +} ``` + +**Viktigt**: +- `.map()`: Loopar över array och returnerar JSX för varje element +- `key={talk.id}`: React behöver unika keys för att effektivt uppdatera listan +- Props nedåt: Parent (SearchPanel) skickar data till child (ResultsTable) + +--- + +#### `frontend/src/components/ChatPanel.tsx` + +AI-chattgränssnitt. + +**State**: +```typescript +const [messages, setMessages] = useState([ + { role: 'assistant', content: 'Hej! Vad vill du veta om riksdagsdebatter?' } +]); +const [input, setInput] = useState(''); // Användarens input + +const chatMutation = useMutation({ + mutationFn: (msgs: ChatMessage[]) => chat(msgs), + onSuccess: (response) => { + // Lägg till AI-svaret i messages + setMessages(prev => [...prev, { role: 'assistant', content: response.reply }]); + } +}); + +const handleSend = () => { + // Lägg till användarens meddelande + const newMessages = [...messages, { role: 'user', content: input }]; + setMessages(newMessages); + setInput(''); // Töm input-fält + + // Skicka till backend + chatMutation.mutate(newMessages); +}; +``` + +**Rendering**: +```tsx +
+ {messages.map((msg, idx) => ( +
+ {msg.role === 'user' ? 'Du' : 'AI'}: +

{msg.content}

+
+ ))} +
+ + setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSend()} +/> + +``` + +--- + +#### `frontend/src/components/VectorSearchPanel.tsx` + +Semantisk sökning via embeddings. + +**Funktion**: +```typescript +const vectorMutation = useMutation({ + mutationFn: (query: string) => vectorSearch(query, 10), // Top 10 resultat + onSuccess: (data) => { + // data.results innehåller liknande anföranden + } +}); + +const handleVectorSearch = () => { + vectorMutation.mutate(searchQuery); +}; +``` + +Resultaten visas i samma format som `ResultsTable`. + +--- + +### TypeScript-typer: `frontend/src/types.ts` + +Definierar data-strukturer som matchar backend. + +```typescript +// Matchar backend/schemas.py SearchRequest +export interface SearchParams { + query: string; + party?: string; + year?: number; + category?: string; + speaker?: string; + sort_by: string; + sort_order: string; + offset: number; + limit: number; +} + +// Matchar backend TalkResult +export interface TalkResult { + id: string; + datum: string; + talare: string; + parti: string; + party_color: string; + anforandetext: string; + snippet: string; + kategori?: string; +} + +// Matchar backend SearchResponse +export interface SearchResponse { + results: TalkResult[]; + total: number; + offset: number; + limit: number; +} +``` + +**Varför TypeScript?** +- Fångar fel vid compile-time (innan kod körs) +- Autocomplete i editorn +- Dokumenterar API:t automatiskt + +--- + +## 🔄 Dataflöde + +### Exempel: Användaren söker efter "skola" + +1. **Frontend** (`SearchPanel.tsx`): + ```typescript + // Användaren skriver "skola" och klickar "Sök" + handleSearch() { + searchMutation.mutate({ + query: "skola", + party: "", + year: "", + sort_by: "date", + sort_order: "desc", + offset: 0, + limit: 50 + }); + } + ``` + +2. **API-klient** (`api.ts`): + ```typescript + // axios skickar POST /api/search med JSON-body + const response = await api.post('/search', params); + ``` + +3. **nginx** (reverse proxy): + ``` + location /api/ { + proxy_pass http://127.0.0.1:8000/api/; + } + ``` + Omdirigerar till backend på port 8000. + +4. **Backend** (`routes/search.py`): + ```python + @router.post("/api/search") + def search_endpoint(request: SearchRequest, db: Session = Depends(get_db)): + # Pydantic validerar request automatiskt + results, total = search_talks( + db=db, + query=request.query, # "skola" + filters={ + "party": request.party, + "year": request.year, + # ... + }, + sort_by=request.sort_by, + sort_order=request.sort_order, + offset=request.offset, + limit=request.limit + ) + + return SearchResponse( + results=results, + total=total, + offset=request.offset, + limit=request.limit + ) + ``` + +5. **Service** (`services/search_service.py`): + ```python + def search_talks(db, query, filters, ...): + # Bygger SQL-fråga + sql = f""" + SELECT {', '.join(select_columns)} + FROM talks + WHERE anforandetext ILIKE %s + """ + params = [f"%{query}%"] # "%skola%" + + # Lägger till filter + if filters.get('party'): + sql += " AND parti = %s" + params.append(filters['party']) + + # Kör fråga + result = db.execute(text(sql), params) + rows = result.fetchall() + + # Genererar snippets + talks = [generate_snippet(row, query) for row in rows] + + return talks, len(talks) + ``` + +6. **PostgreSQL**: + ```sql + SELECT id, datum, talare, parti, anforandetext, ... + FROM talks + WHERE anforandetext ILIKE '%skola%' + ORDER BY datum DESC + LIMIT 50 OFFSET 0; + ``` + +7. **Backend** returnerar JSON: + ```json + { + "results": [ + { + "id": "abc123", + "datum": "2023-11-15", + "talare": "Anna Andersson", + "parti": "S", + "party_color": "#E8112d", + "snippet": "...vi måste investera i skolan..." + }, + ... + ], + "total": 234, + "offset": 0, + "limit": 50 + } + ``` + +8. **Frontend** (`SearchPanel.tsx`): + ```typescript + onSuccess: (data) => { + // react-query cachas svaret + // ResultsTable uppdateras automatiskt med data.results + } + ``` + +9. **Rendering** (`ResultsTable.tsx`): + ```tsx + {results.map(talk => ( + + {talk.datum} + {talk.talare} + {talk.parti} + + + ))} + ``` + +--- + +## 🗄️ Databas & Schema + +### Huvudtabell: `talks` + +**Kolumner** (definierat i `info.py` → `select_columns`): + +```python +select_columns = [ + "id", # Unikt ID (UUID) + "datum", # Datum (YYYY-MM-DD) + "talare", # Talarens namn + "parti", # Partikod (S, M, SD, etc.) + "anforandetext", # Fullständig text + "kategori", # Debattämne + "year", # År (extrakt från datum) + "debatt_titel", # Debattens titel + "intressent_id", # Talarens ID + # ...fler kolumner +] +``` + +**Indexering** (viktigt för prestanda): + +```sql +-- Trigram-index för fritextsökning +CREATE INDEX talks_text_trgm_idx ON talks USING gin(anforandetext gin_trgm_ops); + +-- Index för filter +CREATE INDEX talks_year_idx ON talks(year); +CREATE INDEX talks_parti_idx ON talks(parti); +CREATE INDEX talks_datum_idx ON talks(datum); +``` + +--- + +### Embeddings-tabell: `talk_embeddings` + +**Schema** (skapas i `services/vector_store.py`): + +```sql +CREATE TABLE talk_embeddings ( + id SERIAL PRIMARY KEY, + talk_id TEXT NOT NULL, -- Referens till talks.id + chunk_index INTEGER, -- Vilken chunk (0, 1, 2, ...) + chunk_text TEXT, -- Texten för chunken + embedding vector(3072), -- pgvector-kolumn (embeddinggemma = 3072 dim) + metadata JSONB, -- Extra info (talare, parti, datum) + created_at TIMESTAMP DEFAULT NOW() +); + +-- Index för snabb vector-sökning +CREATE INDEX talk_embeddings_vector_idx ON talk_embeddings +USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- Unique constraint för idempotens +CREATE UNIQUE INDEX talk_embeddings_unique_idx +ON talk_embeddings(talk_id, chunk_index); +``` + +**Hur embeddings fungerar**: + +1. Långt anförande delas upp i chunkar (~500 tokens vardera) +2. Varje chunk får en embedding-vektor (3072 floats) +3. Vektorn representerar semantisk mening +4. Vid sökning: Generera embedding för frågan, hitta närmaste chunkar via cosine similarity + +**Exempel**: + +```python +# Anförande från Anna Andersson (talk_id="abc123") +# Texten är 2000 ord → delas upp i 4 chunkar + +talk_embeddings: +| id | talk_id | chunk_index | chunk_text | embedding | metadata | +|----|---------|-------------|---------------------|-------------------------|---------------------------------| +| 1 | abc123 | 0 | "Vi måste satsa..." | [0.23, -0.45, ...] | {"talare": "Anna", "parti": "S"}| +| 2 | abc123 | 1 | "Skolan behöver..." | [0.12, -0.67, ...] | {"talare": "Anna", "parti": "S"}| +| 3 | abc123 | 2 | "Lärare ska ha..." | [-0.34, 0.89, ...] | {"talare": "Anna", "parti": "S"}| +| 4 | abc123 | 3 | "Sammanfattning..." | [0.45, -0.12, ...] | {"talare": "Anna", "parti": "S"}| +``` + +**Vector-sök SQL**: + +```sql +-- Hitta de 5 mest liknande chunkarna till frågan "lärarutbildning" +SELECT + talk_id, + chunk_text, + metadata, + embedding <=> :query_embedding AS distance -- Cosine distance +FROM talk_embeddings +ORDER BY embedding <=> :query_embedding +LIMIT 5; +``` + +**Operator `<=>`**: pgvector's cosine distance operator (mindre värde = mer likt). + +--- + +## 🤖 AI/LLM-funktionalitet + +### LLM-wrapper: `_llm/llm.py` + +**Vad den gör**: Abstraherar anrop till OpenAI-kompatibla API:er (t.ex. vLLM). + +```python +from _llm import LLM + +llm = LLM(model='vllm') # eller 'gpt-4', 'gpt-3.5-turbo' + +messages = [ + {"role": "system", "content": "Du är en AI som hjälper med riksdagsdebatter."}, + {"role": "user", "content": "Vad säger Socialdemokraterna om skatt?"} +] + +response = llm.chat(messages=messages) +print(response) # AI-genererat svar +``` + +**Konfiguration** (miljövariabler): + +```bash +export LLM_BASE_URL=http://localhost:8000/v1 # vLLM server +export LLM_API_KEY=placeholder # API-nyckel (om krävs) +export CHAT_MODEL=gpt-3.5-turbo # Modell att använda +``` + +**Interna detaljer**: +- Använder `openai`-biblioteket under huven +- Hanterar streaming (om aktiverat) +- Kan konfigurera temperature, max_tokens, etc. + +--- + +### Chat-flöde i backend + +**Fil**: `backend/routes/chat.py` + +```python +@router.post("/api/chat") +def chat_endpoint(request: ChatRequest, db: Session = Depends(get_db)): + """ + AI-chat om riksdagsdebatter. + + Steg: + 1. Extrahera senaste användarfrågan + 2. Sök relevant kontext (vector eller keyword) + 3. Bygg prompt med systemmeddelande + kontext + fråga + 4. Anropa LLM + 5. Returnera svar + """ + + # Extrahera senaste frågan + last_message = request.messages[-1]['content'] + + # Sök kontext + if request.strategy == 'vector': + context = vector_search_for_chat(db, last_message) + elif request.strategy == 'keyword': + context = keyword_search(db, last_message) + else: # auto + context = vector_search_for_chat(db, last_message) + if not context: + context = keyword_search(db, last_message) + + # Bygg prompt + system_message = { + "role": "system", + "content": f"""Du är en AI-assistent som hjälper användare att förstå + svenska riksdagsdebatter. + + Här är relevant information från debatterna: + {context} + + Svara koncist baserat på denna information.""" + } + + messages = [system_message] + request.messages + + # Anropa LLM + llm = LLM(model='vllm') + response = llm.chat(messages=messages) + + return {"reply": response, "context_used": len(context)} +``` + +--- + +### Embeddings-generering + +**Fil**: `scripts/build_embeddings.py` + +**Vad scriptet gör**: +1. Hämtar anföranden från `talks`-tabellen (batchvis med `LIMIT` och `OFFSET`) +2. Delar upp varje anförande i chunkar +3. Genererar embeddings via Ollama +4. Sparar i `talk_embeddings` med `ON CONFLICT DO NOTHING` (idempotent) + +**Körning**: +```bash +# Processa första 200 anföranden python -m scripts.build_embeddings --limit 200 + +# Fortsätt från rad 200 +python -m scripts.build_embeddings --limit 200 --offset 200 + +# Processa alla (varning: kan ta timmar!) +python -m scripts.build_embeddings --limit 1000000 +``` + +**Viktiga funktioner**: + +```python +def process_talk(talk: dict, db: Session): + """ + Processar ett enda anförande. + + Args: + talk: Dict med talk-data (id, anforandetext, talare, etc.) + db: Databas-session + """ + # Chunka texten + chunks = chunk_text(talk['anforandetext'], max_tokens=500) + + # För varje chunk + for idx, chunk in enumerate(chunks): + # Generera embedding + embedding = get_embedding(chunk) + + # Spara till databas + store_embedding( + db=db, + talk_id=talk['id'], + chunk_index=idx, + chunk_text=chunk, + embedding=embedding, + metadata={ + "talare": talk['talare'], + "parti": talk['parti'], + "datum": talk['datum'] + } + ) +``` + +**Progress-tracking**: +``` +Processar anföranden 0-200... +[█████████░░░░░░░░░░░] 47% (94/200) - ETA: 2m 34s +``` + +--- + +## 🔧 Utvecklingsflöde + +### Starta utvecklingsmiljö + +#### 1. Backend +```bash +# Terminal 1 +cd /home/lasse/riksdagen +uvicorn backend.app:app --reload --port 8000 + +# --reload: Startar om automatiskt vid kodändring +# --port 8000: Lyssnar på port 8000 ``` -Kör om med `--offset` för att jobba vidare i batcher. Scriptet använder `ON CONFLICT` så det är idempotent. +**Verifiera**: Gå till `http://localhost:8000/docs` (FastAPI Swagger UI) + +#### 2. Frontend (utvecklingsläge) +```bash +# Terminal 2 +cd /home/lasse/riksdagen/frontend +npm run dev -## Vector- och chat-sök +# Startar Vite dev-server på http://localhost:5173 +# Hot module replacement (HMR) aktiverat +``` -- `POST /api/vector-search` tar `{ "query": "...", "top_k": 5 }` och returnerar närmaste chunkar. -- `POST /api/chat` tar `{ "messages": [...], "strategy": "auto" | "vector" | "keyword" }`. Chatten använder vektorsök först och faller tillbaka på en enkel `ILIKE`-fråga om inget hittas. +#### 3. Bygg för produktion +```bash +# Från projektroten +make frontend -## Var ändrar jag SQL/kolumner? +# Eller manuellt +cd frontend +npm install # Installera dependencies +npm run build # Bygger till frontend/dist/ + +# Ladda om nginx +sudo systemctl reload nginx +``` -- Standard-söket använder funktioner i `backend/routes/search.py` (sök efter `search_talks`). Där kan du t.ex. filtrera bort parti. -- För chatten ligger SQL-frågorna i `backend/routes/chat.py`. Justera `keyword_search` eller `VECTOR_QUERY_SQL` för att lägga till/ta bort fält. +--- + +### Vanliga ändringsscenarier + +#### Scenario 1: Lägg till ny filter-parameter + +**Backend** (`backend/schemas.py`): +```python +class SearchRequest(BaseModel): + # ...existing fields... + municipality: Optional[str] = None # Ny parameter +``` + +**Backend** (`services/search_service.py`): +```python +def search_talks(db, query, filters, ...): + # ...existing code... + + if filters.get('municipality'): + sql += " AND municipality = %s" + params.append(filters['municipality']) +``` + +**Frontend** (`types.ts`): +```typescript +export interface SearchParams { + // ...existing fields... + municipality?: string; // Ny parameter +} +``` + +**Frontend** (`SearchPanel.tsx`): +```tsx +const [municipality, setMunicipality] = useState(''); + +// I JSX + + +// I handleSearch +searchMutation.mutate({ + // ...existing params... + municipality +}); +``` -## Tips för snabbare svar +**Efter ändringar**: +1. Starta om backend (Ctrl+C, kör uvicorn igen) +2. Frontend uppdateras automatiskt (Vite HMR) + +--- + +#### Scenario 2: Lägg till ny endpoint + +**Backend** (`backend/routes/stats.py` – ny fil): +```python +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from backend.database import get_db + +router = APIRouter(prefix="/api", tags=["statistics"]) + +@router.get("/stats/party-distribution") +def get_party_distribution(db: Session = Depends(get_db)): + """ + Returnerar antal anföranden per parti. + """ + result = db.execute(text(""" + SELECT parti, COUNT(*) as count + FROM talks + GROUP BY parti + ORDER BY count DESC + """)) + + return [{"party": row[0], "count": row[1]} for row in result] +``` + +**Backend** (`backend/app.py`): +```python +# ...existing imports... +from backend.routes.stats import router as stats_router + +# ...existing code... +app.include_router(stats_router) +``` + +**Frontend** (`api.ts`): +```typescript +export async function getPartyDistribution(): Promise { + const response = await api.get('/stats/party-distribution'); + return response.data; +} +``` + +**Frontend** (ny komponent `StatsPanel.tsx`): +```typescript +import { useQuery } from '@tanstack/react-query'; +import { getPartyDistribution } from '../api'; + +function StatsPanel() { + const { data, isLoading } = useQuery({ + queryKey: ['party-distribution'], + queryFn: getPartyDistribution + }); + + if (isLoading) return
Laddar...
; + + return ( +
+

Anföranden per parti

+
    + {data?.map(item => ( +
  • + {item.party}: {item.count} +
  • + ))} +
+
+ ); +} +``` + +--- + +#### Scenario 3: Ändra databas-schema + +**1. Manuell ändring** (för utveckling): +```sql +-- Koppla till PostgreSQL +psql -U riksdagen -d riksdagen + +-- Lägg till kolumn +ALTER TABLE talks ADD COLUMN sentiment VARCHAR(20); + +-- Uppdatera info.py +# Lägg till "sentiment" i select_columns +``` + +**2. För produktion: Använd migreringsverktyg (Alembic)** + +```bash +# Installera Alembic +pip install alembic + +# Initiera +alembic init alembic + +# Skapa migration +alembic revision -m "Add sentiment column" + +# Redigera filen i alembic/versions/ +def upgrade(): + op.add_column('talks', sa.Column('sentiment', sa.String(20))) + +def downgrade(): + op.drop_column('talks', 'sentiment') + +# Kör migration +alembic upgrade head +``` + +--- + +## 🐛 Felsökning + +### Problem: Frontend visar 502 Bad Gateway + +**Orsak**: nginx kan inte nå backend. + +**Lösning**: +```bash +# 1. Kolla att backend körs +curl http://127.0.0.1:8000/healthz + +# Om inget svar: +uvicorn backend.app:app --reload --port 8000 + +# 2. Kolla nginx-konfiguration +sudo nginx -t + +# 3. Kolla nginx-loggar +sudo tail -f /var/log/nginx/error.log +``` + +--- + +### Problem: TypeScript-fel i frontend + +**Fel**: `Property 'X' does not exist on type 'Y'` + +**Orsak**: Type mismatch mellan frontend och backend. + +**Lösning**: +1. Kolla backend-schema i `backend/schemas.py` +2. Uppdatera motsvarande interface i `frontend/src/types.ts` +3. Kör `npm run build` för att verifiera + +--- + +### Problem: Inget resultat från vector-sök + +**Orsak**: Inga embeddings genererade. + +**Lösning**: +```bash +# 1. Kolla att tabellen finns +psql -U riksdagen -d riksdagen -c "SELECT COUNT(*) FROM talk_embeddings;" + +# Om 0 rader: +python -m scripts.build_embeddings --limit 100 + +# 2. Kolla att Ollama körs +curl http://192.168.1.11:11434/api/tags + +# 3. Testa embedding-generering +python -c " +from backend.services.embedding import get_embedding +emb = get_embedding('test') +print(len(emb)) # Ska ge 3072 +" +``` + +--- + +### Problem: Chat-svar är irrelevanta + +**Orsak**: Dålig kontext från sök. + +**Debug**: +```python +# I backend/routes/chat.py, lägg till logging +import logging +logger = logging.getLogger(__name__) + +# I chat_endpoint +logger.info(f"Context found: {len(context)} characters") +logger.info(f"First 200 chars: {context[:200]}") + +# Kör backend med log-level DEBUG +uvicorn backend.app:app --reload --log-level debug +``` + +**Lösning**: +1. Öka `top_k` i vector-sök (fler resultat) +2. Förbättra chunkning-strategi (överlappande chunkar) +3. Justera prompt i system-message + +--- + +### Problem: Långsam sökning + +**Orsak**: Saknade index. + +**Lösning**: +```sql +-- Kolla existerande index +SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'talks'; + +-- Lägg till saknade index +CREATE INDEX talks_text_trgm_idx ON talks USING gin(anforandetext gin_trgm_ops); +CREATE INDEX talks_year_idx ON talks(year); +CREATE INDEX talks_parti_idx ON talks(parti); + +-- För vector-sök +CREATE INDEX talk_embeddings_vector_idx ON talk_embeddings +USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); +``` + +**Optimering**: +```python +# I services/search_service.py +# Begränsa kolumner som returneras +select_columns = ["id", "datum", "talare", "parti", "snippet"] # Inte hela anforandetext + +# Använd paginering +LIMIT 50 OFFSET 0 # Hämta bara 50 rader åt gången +``` -- Lägg index på `talks(anforandetext gin_trgm_ops)`, `talks(year)` och kolumner du filtrerar på. -- Begränsa antalet rader med `LIMIT` och paginering i REST-svaret (`offset`, `limit`). -- Cacha frekventa frågor (t.ex. i Redis) om du märker återkommande mönster. +--- -## PostgreSQL & vektorer +## 📝 Sammanfattning: Viktiga filer och deras roller -- Tabellens kolumnlista finns i `info.py` (`select_columns`). -- pgvector-kolumnen heter `embedding` (läggs till manuellt). -- Skript för att mata in embeddingar kan ligga i `scripts/` (se exempel i tidigare konversation). +| Fil | Vad den gör | +|-----|-------------| +| `backend/app.py` | Root för FastAPI-app, registrerar alla routes | +| `backend/database.py` | Databaskoppling, session-factory, aktiverar pgvector | +| `backend/routes/search.py` | Endpoint för nyckelordssökning | +| `backend/routes/chat.py` | Endpoints för AI-chat och vector-sök | +| `backend/services/search_service.py` | SQL-frågor för sökning, snippet-generering | +| `backend/services/vector_store.py` | pgvector-operationer, likhetssökning | +| `backend/services/embedding.py` | Chunkning, embedding-generering via Ollama | +| `frontend/src/App.tsx` | Root-komponent, mode-switching (sök/chat/vector) | +| `frontend/src/api.ts` | Axios-baserad API-klient | +| `frontend/src/components/SearchPanel.tsx` | Sökformulär med filter | +| `frontend/src/components/ChatPanel.tsx` | Chat-gränssnitt | +| `frontend/src/components/ResultsTable.tsx` | Tabellvisning av resultat | +| `scripts/build_embeddings.py` | Genererar embeddings för alla anföranden | +| `_llm/llm.py` | Wrapper för OpenAI-kompatibla LLM-anrop | +| `info.py` | Konstanter (partier, färger, kolumner) | +| `config.py` | Databas-URL och konfiguration | -## Felsökning +--- -- Tomt resultat trots 200 OK → kontrollera webbkonsolen; frontenden visar nu ett meddelande när inga träffar hittas. -- 502 via nginx → kontrollera att backend kör (`curl http://127.0.0.1:8000/healthz`) och att senaste frontend-bygget ligger i `frontend/dist`. -- Vid ENOSPC för filbevakare → öka `fs.inotify.max_user_watches` eller använd `usePolling` (redan aktiverat i `vite.config.ts`). +## 🚀 Nästa steg: Förslag på förbättringar -## Förslag: filtrering, vector-sök och chat +1. **Streaming för chat**: Visa AI-svaret ord för ord (WebSocket eller Server-Sent Events) +2. **Caching**: Spara frekventa frågor i Redis för snabbare svar +3. **Feedback-loop**: Använd användarfeedback för att förbättra sökresultat +4. **Avancerad filtrering**: Datum-intervall, flera partier samtidigt +5. **Export-funktion**: Ladda ner resultat som CSV/PDF +6. **Visualiseringar**: Diagram för partier, tidslinjer, sentiment-analys -| Funktion | Vad saknas | Förslag | -| -------- | ---------- | ------- | -| Snabbare ord-sök | Index på `talks`-tabellen (t.ex. `CREATE INDEX` på kolumner du filtrerar/ordnar på) | Lägg till index via en migrationsfil. Begränsa svaret (paginering, `LIMIT`). | -| Vector-sök | Inga embeddings genereras ännu | 1) Batcha anföranden genom ett script som kallar Ollama `embeddinggemma` (t.ex. `scripts/build_embeddings.py`). 2) Spara till Postgres (`pgvector`) eller en filbaserad FAISS-index. 3) Exponera ny backend-rout `POST /api/search/vector`. | -| Chat | Ingen backend-endpoint och UI-switchn saknas | Skapa nytt frontendläge `mode = "chat"` (t.ex. toggle i `SearchPanel`). Lägg till backend-endpoint som hämtar relevanta tal via vector-sök och skickar prompt till vLLM/OpenAI. Cacha konversationer i t.ex. Redis för snabbare uppföljningar. | +--- -### Så här bygger du vidare +## 🆘 Hjälp och support -1. Skapa script som läser tal (`backend/services/search_service.py`) och producerar embeddings genom att ropa till `http://192.168.1.11:11434/api/embeddings` med modellen `embeddinggemma`. -2. Spara resultat i tabell `talk_embeddings` (`_id`, `vector`, `metadata`). -3. Implementera ny FastAPI-rout `POST /api/chat` som: - - Tar emot fråga + historik. - - Kör vector-sök (k-NN) för att hämta relevanta anföranden. - - Bygger prompt och anropar vLLM-servern (OpenAI-kompatibel). -4. Frontend: lägg till switch i `SearchPanel` och skapa en ny komponent `ChatPanel` som använder `react-query`-mutation mot `/api/chat`. +- **Backend-loggar**: `uvicorn backend.app:app --log-level debug` +- **Frontend-konsol**: Öppna DevTools (F12) i webbläsaren +- **Databas-frågor**: `psql -U riksdagen -d riksdagen` +- **API-dokumentation**: `http://localhost:8000/docs` (när backend körs) -Lycka till! Ange gärna förbättringsförslag via feedback-formuläret i appen. +Lycka till med utvecklingen! 🎉