Search, chat and research over parliamentary speeches and documents
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

486 lines
12 KiB

export type TalkHit = {
id: string; // ArangoDB _id, but named 'id' in TypeScript
_id?: string; // Some endpoints (e.g. chat search cards) return the raw ArangoDB _id under this key instead
doc_id?: string; // Made optional since it might not always be present
text: string;
snippet?: string | null;
snippet_long?: string | null;
number?: number | null;
debate_type?: string | null;
speaker?: string | null;
date?: string | null;
year?: number | null;
url_session?: string | null;
party?: string | null;
url_audio?: string | null;
audio_start_seconds?: number | null;
person_id?: string | null;
title?: string | null;
activity_type?: string | null;
related_doc_id?: string | null;
};
export type SearchFilters = {
parties: string[];
people: string[];
debates: string[];
from_year?: number | null;
to_year?: number | null;
};
export type SearchRequest = SearchFilters & {
q: string;
limit?: number;
include_snippets?: boolean;
speaker?: string | null;
speaker_ids?: string[]; // Accept a list of speaker IDs for filtering
};
export type AggregatedStats = {
per_party: Record<string, number>;
per_year: Record<number, number>;
total: number;
};
export type SearchResponse = {
results: TalkHit[];
stats: AggregatedStats;
active_filters: SearchFilters;
limit_reached: boolean;
generated_at: string;
};
export type DebateType = {
title: string;
description: string;
};
export type Party = {
code: string;
name: string;
color: string;
active: boolean;
};
export type MetaResponse = {
parliament: {
name: string;
name_en: string;
country: string;
data_start_year: number;
};
parties: Party[];
party_defaults: { unknown_color: string; code_pattern: string };
/** Brand tokens injected as CSS custom properties; see parliament.yaml `theme:`. */
theme?: Record<string, string>;
/** Keyed by the value stored in speeches.activity_type. */
activity_types: Record<string, DebateType>;
vocabulary: Record<string, string>;
urls: Record<string, string>;
site: {
title: string;
tagline: string;
explainer: string;
limit_warning: string;
contact: { email: string | null; url: string | null };
};
};
export type ChatMessage = {
role: "system" | "user" | "assistant";
content: string;
};
export type ChatSource = {
_id: string;
chunk_index: number;
heading: string | null;
url_video: string | null;
snippet: string;
score: number;
speaker?: string | null;
party?: string | null;
person_id?: string | null;
date?: string | null;
};
export type MotionAuthor = {
name: string | null;
party: string | null;
role: string | null;
person_id: string | null;
first_name?: string | null;
image_url_medium?: string | null;
constituency?: string | null;
status?: string | null;
};
export type MotionYrkande = {
number?: string | null;
text?: string | null;
committee_recommendation?: string | null;
chamber_decision?: string | null;
handled_in?: string | null;
};
export type Motion = {
kind: "motion";
doc_id: string;
speaker_name: string;
party: string;
date: string | null;
title: string | null;
subtitle: string | null;
text: string | null;
has_text: boolean;
session_label: string | null;
designation: string | null;
subtype: string | null;
committee: string | null;
status: string | null;
parties: string[];
authors: MotionAuthor[];
yrkanden: MotionYrkande[];
url_pdf: string | null;
url_html: string | null;
person?: {
image_url_medium?: string | null;
first_name?: string | null;
person_id?: string | null;
constituency?: string | null;
status?: string | null;
} | null;
};
export type ProviderOverride = {
provider_id: string;
api_key: string;
smart_model?: string; // model for orchestrator + communicator
fast_model?: string; // model for summarisation (defaults to smart_model)
editor_model?: string; // model for the editor pass (defaults to smart_model)
};
/**
* Extras every research endpoint that spawns a job accepts. Both ride the
* backend's stdin-only "secrets" channel and are never persisted: board_key
* decrypts the board, llm points the job at the user's own provider.
*/
export type ResearchSpawnExtras = {
board_key?: string;
llm?: ProviderOverride;
};
export type ChatRequest = {
messages: ChatMessage[];
top_k: number;
focus_ids?: string[]; // Optional narrowing of future searches.
session_id?: string; // Propagates the browser session to the backend.
provider_override?: ProviderOverride; // User-supplied provider; key stored only in browser.
use_editor?: boolean; // Run the editor fact-check + language pass.
};
export type ChatResponseTable = {
results: TalkHit[];
stats: {
per_party: Record<string, number>;
per_year: Record<number, number>;
total: number;
};
limit_reached: boolean;
return_snippets: boolean;
focus_ids: string[];
};
export type LiveSearchCard = {
type: "search_card";
message?: string; // insight text shown as card header (from shadow communicator)
query: string;
results: TalkHit[];
total: number;
limit_reached: boolean;
stats?: {
per_party: Record<string, number>;
per_year: Record<number, number>;
};
speaker_ids?: string[];
speaker_ids_context?: string;
};
export type LiveStatsCard = {
type: "stats_card";
message?: string; // insight text shown as card header (from shadow communicator)
rows: Record<string, string | number>[];
speaker_ids?: string[];
speaker_ids_context?: string;
};
export type LiveInsightCard = {
type: "insight_card";
message: string;
sources?: Record<string, string>; // talk ID → url_video
speaker_ids: string[];
speaker_ids_context?: string;
};
export type LiveCard = LiveSearchCard | LiveStatsCard | LiveInsightCard;
/**
* One research step shown to the user during (and after) LLM research.
* Starts as a "thinking" card with just a message, then gets upgraded to
* a result card (search/stats) or the final answer card.
*/
export type ResearchCard = {
id: string;
message: string; // LLM's narration ("Söker efter X…")
result?: LiveCard; // upgraded to this when surface_results fires
isAnswer: boolean; // upgraded to true when final answer arrives
answerHtml?: string; // rendered answer HTML (when isAnswer=true)
};
export type PersonRef = {
person_id: string;
name: string;
party: string;
};
export type ChatReply = {
answer: string;
sources: ChatSource[];
persons?: PersonRef[];
tables?: ChatResponseTable[];
focus_ids?: string[];
};
export type ChatResponse = ChatReply;
export type ChatTurn = {
id: string;
question: string;
answer?: string;
answerHtml?: string;
sources?: ChatSource[];
persons?: PersonRef[];
tables?: ChatResponseTable[];
liveCards?: ResearchCard[];
createdAt: string;
status: "pending" | "ready" | "error";
errorMessage?: string;
};
export type Uppdrag = {
typ?: string | null;
organ_kod?: string | null;
roll_kod?: string | null;
status?: string | null;
uppgift?: string | null;
from?: string | null;
tom?: string | null;
};
export type PersonDetail = {
person_id: string;
name: string;
first_name?: string | null;
last_name?: string | null;
party?: string | null;
constituency?: string | null;
status?: string | null;
image_url_medium?: string | null;
birth_year?: string | null;
uppdrag?: Uppdrag[] | null;
};
export type MpChatTurn = {
id: string;
question: string;
/** Raw markdown answer, kept alongside the rendered HTML for markdown export. */
answer?: string;
answerHtml?: string;
sources?: ChatSource[];
status: "pending" | "ready" | "error";
errorMessage?: string;
};
export type SnapshotTurn = {
question: string;
answerHtml: string;
sources?: ChatSource[];
};
export type SnapshotData = {
id: string;
session_type: "general" | "mp";
person_id?: string | null;
initial_speech_id?: string | null;
turns: SnapshotTurn[];
llm_messages?: ChatMessage[];
focus_ids?: string[];
created_at: string;
};
export type ChatSessionData = {
id: string;
session_type: "general" | "mp";
person_id?: string | null;
initial_speech_id?: string | null;
llm_messages: ChatMessage[];
turns: ChatTurn[] | MpChatTurn[];
focus_ids: string[];
/** Owned sessions: all content as one client-encrypted blob; plaintext fields empty. */
enc_payload?: string | null;
};
/** PUT body for /sessions — either the plaintext fields or the enc_* pair. */
export type SessionUpsertData = {
session_type: "general" | "mp";
person_id?: string | null;
initial_speech_id?: string | null;
llm_messages?: ChatMessage[];
turns?: ChatTurn[] | MpChatTurn[] | SnapshotTurn[];
focus_ids?: string[];
enc_payload?: string;
enc_title?: string;
};
/** Decrypted contents of ChatSessionData.enc_payload. */
export type EncSessionPayload = {
llm_messages: ChatMessage[];
turns: ChatTurn[] | MpChatTurn[];
focus_ids: string[];
person_id?: string | null;
initial_speech_id?: string | null;
};
/** Decrypted contents of enc_title (list labels + MP link target). */
export type EncTitlePayload = {
title: string;
person_id?: string | null;
};
/* ── Accounts (zero-knowledge login) ───────────────────────────── */
export type PreloginResponse = {
kdf_salt: string;
kdf_iterations: number;
};
export type AuthResponse = {
token: string;
user_id: string;
username: string;
wrapped_dek: string;
kdf_salt: string;
kdf_iterations: number;
};
export type MyChatRow = {
id: string;
session_type: "general" | "mp";
enc_title: string | null;
created_at: string;
last_activity: string;
};
/* ── Deep research ─────────────────────────────────────────────── */
export type ResearchFinding = {
label: string;
detail?: string;
quote?: string;
source_id?: string;
speaker?: string | null;
party?: string | null;
date?: string | null;
};
export type ResearchLead = {
kind: "search" | "person" | "debate";
target: string;
lead?: string;
label?: string | null;
};
export type ResearchThread = {
id: string;
title: string;
question: string;
why?: string;
origin: "auto" | "seed";
depth: number;
status: "proposed" | "active" | "archived";
pinned: boolean;
findings: ResearchFinding[];
open_questions: string[];
leads: ResearchLead[];
/** User's free-text steer for the thread (feeds its trips). */
guidance?: string | null;
/** Synthesized markdown answer with [källa:ID] citations; null until dug. */
answer?: string | null;
/** Depth at which `answer` was last written — answer is stale when depth > answer_depth. */
answer_depth?: number;
/** Discovery search hints, used until the thread's first trip. */
hints?: string[];
created_at: string;
updated_at: string;
};
export type ResearchJob = {
job_id: string;
kind: string;
status: string;
progress?: { done: number; total: number; current: string };
started_at?: string;
};
export type ResearchBoardSummary = {
id: string;
title: string;
topic: string;
status: "new" | "scouting" | "awaiting" | "digging" | "reporting" | "ready" | "failed";
revision: number;
thread_count: number;
updated_at: string;
created_at: string;
/** Encrypted board: title/topic are "v1:..." ciphertext; decrypt with the unwrapped board key. */
enc?: boolean;
wrapped_board_key?: string | null;
};
export type ResearchBoardDetail = {
id: string;
title: string;
topic: string;
intro?: string | null;
status: "new" | "scouting" | "awaiting" | "digging" | "reporting" | "ready" | "failed";
revision: number;
target_depth: number;
threads: ResearchThread[];
job?: ResearchJob | null;
/** Single regeneratable markdown report woven from the thread answers. */
report?: string | null;
report_generated_at?: string | null;
created_at: string;
updated_at: string;
enc?: boolean;
wrapped_board_key?: string | null;
};
export type ResearchEvent = {
done: number;
total: number;
current?: string;
message?: string;
level?: string;
finding?: { label: string; detail?: string };
/** Encrypted boards: the content fields above arrive as one ciphertext JSON blob. */
enc?: string;
};
export type ResearchEventsResponse = {
events: ResearchEvent[];
is_done: boolean;
offset: number;
status: string;
progress: { done: number; total: number; current: string };
errors: string[];
};