# Riksdagen Sökplattform – Teknisk Översikt 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. --- ## 📋 Innehållsförteckning 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) --- ## 🏗️ Systemöversikt ``` ┌─────────────┐ 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) - 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. --- ## ⚛️ Frontend-arkitektur - **`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. --- ## 🔄 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 | |-----|----------| | `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 } // 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` Sökformulär med filter. **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 }); }; ``` **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..." /> ``` **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 --- #### `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) => ( ))}
Datum Talare Parti Innehå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 ``` **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 # Startar Vite dev-server på http://localhost:5173 # Hot module replacement (HMR) aktiverat ``` #### 3. Bygg för produktion ```bash # Från projektroten make frontend # Eller manuellt cd frontend npm install # Installera dependencies npm run build # Bygger till frontend/dist/ # Ladda om nginx sudo systemctl reload nginx ``` --- ### 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 }); ``` **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 ``` --- ## 📝 Sammanfattning: Viktiga filer och deras roller | 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 | --- ## 🚀 Nästa steg: Förslag på förbättringar 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 --- ## 🆘 Hjälp och support - **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 med utvecklingen! 🎉