Skip to content

Commit c2ab02e

Browse files
committed
Updates to _core, improvements to research workflow
- Improve stability, performance - Extend config_app.yaml, e.g. more models tested - Refactor app UI texts to separate file - Track token usage of last research step - Log input to research writing to JSON files - Bug fixes - Resolve some minor issues in UI
1 parent af4b180 commit c2ab02e

File tree

13 files changed

+226
-129
lines changed

13 files changed

+226
-129
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,7 @@ cython_debug/
172172

173173
# PyPI configuration file
174174
.pypirc
175+
176+
# Additional files to ignore
175177
.DS_Store
176178
*.docx
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

01_data/_weaviate_data/schema.db

0 Bytes
Binary file not shown.

02_app/_core/app_info.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
INFO_TEXT_MODAL = """Mit dieser App kannst du **vertieft über eigene Dokumentsammlungen recherchieren**.
2+
3+
Die App dient zum Testen. **Beachte, dass Sprachmodelle (LLMs) Fehler machen und die Ergebnisse fehlerhaft oder unvollständig sein können.** Überprüfe die Ergebnisse immer.
4+
5+
Deine Fragen werden an Clouddienste weitergeleitet und dort verarbeitet. **Gib daher nur als öffentlich klassifizierte Informationen als Fragen ein!** Beachte auch, dass die Nutzung anonymisiert aufgezeichnet wird und Mitarbeitende vom Statistischen Amt Eingaben stichprobenartig überprüfen, um die App zu verbessern.
6+
7+
Zu Demonstrationszwecken bezieht die App für die Antworten eine kleine Auswahl von [Kantonsratsprotokollen des Kantons Zürich ein](https://opendata.swiss/de/dataset/zurcher-kantonsratsprotokolle-des-19-jahrhunderts).
8+
9+
Verantwortlich: Statistisches Amt, [Team Data](mailto:[email protected]).
10+
11+
App-Version v0.2. Letzte Aktualisierung 23.7.2025
12+
13+
### Wie funktioniert die App?
14+
15+
Die App arbeitet in mehreren Schritten:
16+
17+
1. **Suchanfragen formulieren**: Basierend auf deiner Fragestellung generiert die App gezielte Suchanfragen.
18+
2. **Recherche durchführen**: Anschließend durchsucht die App die Dokumente nach passenden Textstellen.
19+
3. **Relevanz prüfen**: Die gefundenen Passagen werden daraufhin geprüft, ob sie für deine Fragestellung von Bedeutung sind.
20+
4. **Ganze Beschlüsse analysieren**: Zu relevanten Textstellen werden die Dokumente im Volltext analysiert und inhaltlich in Bezug auf die Frage zusammengefasst.
21+
5. **Recherchestand reflektieren**: Falls die iterative Recherche aktiviert ist, bewertet die App den bisherigen Erkenntnisstand und entscheidet, ob weitere Recherchen notwendig sind.
22+
6. **Abschlussbericht erstellen**: Die App fasst die Ergebnisse in einem Abschlussbericht zusammen.
23+
""".strip()
24+
25+
INFO_TEXT_SIDEBAR = """
26+
Recherchewerkzeug für eigene Dokumentsammlungen.\n\n:red[Achtung: Dies ist ein experimenteller Prototyp. Gib nur als öffentlich klassifizierte Daten als Fragen ein. Die Ergebnisse können fehlerhaft oder unvollständig sein. **Überprüfe die Ergebnisse immer.**] \n\n Die Bearbeitung kann einige Minuten dauern, abhängig von der Komplexität der Anfrage und der Anzahl der relevanten Dokumente.
27+
""".strip()
28+
29+
SAMPLE_QUERY = "Was hat der Kantonsrat zu Steuern entschieden?"

02_app/_core/llm_client.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from abc import ABC, abstractmethod
2-
from typing import Optional, Dict, Any, Iterator
2+
from typing import Optional, Dict, Any
33
import os
4+
import requests
5+
import json
6+
from datetime import datetime
47
from openai import OpenAI
58
from tenacity import retry, stop_after_attempt, wait_random_exponential
69
from dotenv import load_dotenv
710
from _core.config import config
811
from _core.logger import custom_logger
12+
from _core.utils import TokenCounter
913

1014

1115
try:
@@ -32,8 +36,8 @@ def call_structured(
3236
pass
3337

3438
@abstractmethod
35-
def call_streamed(self, prompt: str, **kwargs) -> Iterator:
36-
"""Stream responses from the LLM."""
39+
def call_with_reasoning(self, prompt: str, **kwargs) -> tuple[str, dict]:
40+
"""Call LLM API with reasoning parameters."""
3741
pass
3842

3943

@@ -78,7 +82,7 @@ def _call():
7882
completion = self.client.chat.completions.create(
7983
model=model_id or config["models"]["performance_low"],
8084
temperature=temperature or config["temperature"]["low"],
81-
max_tokens=max_tokens or config["llm"]["max_tokens"],
85+
max_tokens=max_tokens or config["llm"]["max_tokens_output"],
8286
reasoning_effort=reasoning_effort,
8387
messages=[{"role": "user", "content": prompt}],
8488
**kwargs,
@@ -104,7 +108,7 @@ def _call():
104108
completion = self.client.chat.completions.create(
105109
model=model_id or config["models"]["performance_low"],
106110
temperature=temperature or config["temperature"]["low"],
107-
max_tokens=max_tokens or config["llm"]["max_tokens"],
111+
max_tokens=max_tokens or config["llm"]["max_tokens_output"],
108112
response_format={
109113
"type": "json_schema",
110114
"json_schema": {
@@ -126,32 +130,71 @@ def _call():
126130

127131
return _call()
128132

129-
def call_streamed(
133+
def call_with_reasoning(
130134
self,
131135
prompt: str,
132136
model_id: str = None,
133137
temperature: float = None,
134138
max_tokens: int = None,
135-
**kwargs,
136-
) -> Iterator:
137-
"""Stream responses from OpenRouter."""
139+
) -> tuple[str, dict]:
140+
"""Call LLM API with reasoning parameters."""
141+
142+
# At the moment, only the Gemini 2.5 model support context lengths beyond 200k.
143+
# Here we check if the model is not Gemini 2.5 and if the token count exceeds the fallback limit.
144+
# If so, we switch to the fallback model.
145+
if "google/gemini-2.5" not in model_id or config["models"]["performance_high"]:
146+
token_count = TokenCounter.count_tokens(prompt)
147+
if token_count > config["llm"]["fallback_token_limit"]:
148+
custom_logger.info_console(
149+
f"Token count ({token_count}) exceeds fallback limit. Using fallback model."
150+
)
151+
model_id = config["models"]["fallback"]
138152

139153
custom_logger.info_console(
140-
f"Streaming response for prompt: {prompt[:50]}... (model: {model_id or config['models']['performance_low']})"
154+
f"Calling API with prompt: {prompt[:200]}... (model: {model_id or config['models']['performance_high']})"
141155
)
142156

143-
@self._retry
144-
def _stream():
145-
return self.client.chat.completions.create(
146-
model=model_id or config["models"]["performance_low"],
147-
temperature=temperature or config["temperature"]["low"],
148-
max_tokens=max_tokens or config["llm"]["max_tokens"],
149-
stream=True,
150-
messages=[{"role": "user", "content": prompt}],
151-
**kwargs,
152-
)
153-
154-
return _stream()
157+
# Since the final call is costly, we do not retry it, if it fails.
158+
# If you want to retry it, uncomment the decorator below.
159+
# @self._retry
160+
def _call_model():
161+
try:
162+
url = "https://openrouter.ai/api/v1/chat/completions"
163+
payload = {
164+
"model": model_id or config["models"]["performance_high"],
165+
"messages": [{"role": "user", "content": prompt}],
166+
"temperature": temperature or config["temperature"]["low"],
167+
"max_tokens": max_tokens or config["llm"]["max_tokens_output"],
168+
# Adjust this according to the model specifications. Details:
169+
# https://openrouter.ai/docs/use-cases/reasoning-tokens
170+
"reasoning": {
171+
"max_tokens": -1,
172+
# "effort": "high",
173+
},
174+
}
175+
headers = {
176+
"Authorization": f"Bearer {self.api_key}",
177+
"Content-Type": "application/json",
178+
}
179+
response = requests.post(url, json=payload, headers=headers)
180+
181+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
182+
with open(
183+
f"{config['app']['save_final_docs_to']}response_{timestamp}.json",
184+
"w",
185+
) as f:
186+
json.dump(response.json(), f)
187+
188+
response = response.json()
189+
usage = response.get("usage", {})
190+
response = response["choices"][0]["message"]["content"]
191+
return response, usage
192+
except Exception as e:
193+
custom_logger.info_console(f"Error during final reasoning: {e}")
194+
return "", {}
195+
196+
response, usage = _call_model()
197+
return response, usage
155198

156199

157200
class ClientManager:

02_app/_core/llm_processing.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pandas as pd
22
import json
33
import re
4-
from typing import List, Iterator, Union, Dict, Any
4+
from datetime import datetime
5+
from typing import List, Union, Dict, Any
56
from _core.config import config
67
from _core.models import ReflectTask, RelevanceCheck
78
from _core.logger import custom_logger
@@ -224,8 +225,8 @@ def reflect_task_status(
224225
def create_final_report(
225226
user_query: str,
226227
final_docs: pd.DataFrame,
227-
model_id: str = config["models"]["performance_medium"],
228-
) -> Iterator[str]:
228+
model_id: str = config["models"]["performance_high"],
229+
) -> tuple[str, dict]:
229230
"""Generate a final research report from selected documents."""
230231
research_results = [
231232
DOCUMENT.format(
@@ -241,14 +242,17 @@ def create_final_report(
241242
custom_logger.info_console(
242243
f"Creating final report for query: {user_query} with {len(research_results)} documents."
243244
)
245+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
246+
with open(f"{config['app']['save_final_docs_to']}final_docs_{timestamp}.json", "w") as f:
247+
json.dump(research_results, f, indent=2)
244248

245249
research_results_text = "\n\n".join(research_results)
246250

247-
return llm_client.call_streamed(
251+
response, usage = llm_client.call_with_reasoning(
248252
prompt=RESEARCH_WRITER.format(
249253
user_query=user_query, research_results=research_results_text
250254
),
251255
model_id=model_id,
252256
temperature=config["temperature"]["base"],
253-
reasoning_effort=config["llm"]["reasoning_effort"],
254257
)
258+
return response, usage

02_app/_core/prompts.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,3 @@
1-
INFO_TEXT = """Mit dieser App kannst du **vertieft über eigene Dokumentsammlungen recherchieren**.
2-
3-
Die App dient zum Testen. **Beachte, dass Sprachmodelle (LLMs) Fehler machen und die Ergebnisse fehlerhaft oder unvollständig sein können.** Überprüfe die Ergebnisse immer.
4-
5-
Deine Fragen werden an Clouddienste weitergeleitet und dort verarbeitet. **Gib daher nur als öffentlich klassifizierte Informationen als Fragen ein!** Beachte auch, dass die Nutzung anonymisiert aufgezeichnet wird und Mitarbeitende vom Statistischen Amt Eingaben stichprobenartig überprüfen, um die App zu verbessern.
6-
7-
Zu Demonstrationszwecken bezieht die App für die Antworten eine kleine Auswahl von [Kantonsratsprotokollen des Kantons Zürich ein](https://opendata.swiss/de/dataset/zurcher-kantonsratsprotokolle-des-19-jahrhunderts).
8-
9-
Verantwortlich: Statistisches Amt, [Team Data](mailto:[email protected]).
10-
11-
App-Version v0.1. Letzte Aktualisierung 17.7.2025
12-
13-
### Wie funktioniert die App?
14-
15-
Die App arbeitet in mehreren Schritten:
16-
17-
1. **Suchanfragen formulieren**: Basierend auf deiner Fragestellung generiert die App gezielte Suchanfragen.
18-
2. **Recherche durchführen**: Anschließend durchsucht die App die Dokumente nach passenden Textstellen.
19-
3. **Relevanz prüfen**: Die gefundenen Passagen werden daraufhin geprüft, ob sie für deine Fragestellung von Bedeutung sind.
20-
4. **Ganze Beschlüsse analysieren**: Zu relevanten Textstellen werden die gesamten Beschlüsse analysiert und inhaltlich zusammengefasst.
21-
5. **Recherchestand reflektieren**: Die App bewertet den bisherigen Erkenntnisstand und entscheidet, ob eine weitere Recherche notwendig ist.
22-
6. **Abschlussbericht erstellen**: Sobald ausreichend Informationen vorliegen oder die maximale Anzahl von drei Iterationen erreicht ist, fasst die App die Ergebnisse in einem Abschlussbericht zusammen.
23-
"""
24-
251
CREATE_QUERIES = """
262
Du bist ein Rechercheassistent, spezialisiert auf Dokumente vom Kantonsrat Zürich.
273
@@ -86,10 +62,13 @@
8662
- Beziehe nur Informationen aus den Suchergebnissen ein, erfinde nichts.
8763
8864
Hier ist die Frage bzw. Fragen des Experten:
65+
<expertenfrage>
8966
{user_query}
67+
</expertenfrage>
9068
9169
Hier das Dokument vom Kanton Zürich:
9270
71+
<beschluss>
9372
Titel
9473
{title}
9574
@@ -101,20 +80,25 @@
10180
10281
Dokument-Text
10382
{text}
83+
</beschluss>
10484
""".strip()
10585

10686

10787
DOCUMENT = """
88+
Regierungsratsbeschluss Kanton Zürich
10889
Dokument Kanton Zürich
10990
{title}
11091
{date}
11192
{link}
93+
94+
Analyseergebnisse und Zusammenfassung der relevanten Informationen aus dem Dokument in Bezug auf die Frage(n) des Experten:
11295
{analysis}
11396
""".strip()
11497

11598

11699
REFLECT_TASK = """
117100
Du bist ein Rechercheassistent, spezialisiert auf Dokumente des Kantons Zürich.
101+
118102
Deine Aufgabe ist es, den aktuellen Stand einer Recherche zu reflektieren und zu entscheiden, ob weitere Schritte erforderlich sind oder ob die Recherche abgeschlossen werden kann.
119103
120104
Wichtige Hinweise:
@@ -129,15 +113,22 @@
129113
- False: Es sind weitere Schritte erforderlich, um die Fragen vollständig zu beantworten.
130114
131115
Hier ist die Frage bzw. Fragen des Experten:
116+
<expertenfrage>
132117
{user_query}
118+
</expertenfrage>
133119
134120
Hier sind die Analyseergebnisse von relevanten Dokumenten, die bisher erarbeitet wurden:
121+
<analyseergebnisse>
135122
{research_results}
136-
"""
123+
</analyseergebnisse>
124+
125+
Reflektiere jetzt den aktuellen Stand der Recherche und entscheide, ob weitere Schritte erforderlich sind oder ob die Recherche abgeschlossen werden kann.
126+
""".strip()
137127

138128

139129
RESEARCH_WRITER = """
140130
Du bist ein Rechercheassistent, spezialisiert auf Dokumente vom Kanton Zürich.
131+
141132
Deine Aufgabe ist es, die Ergebnisse einer Recherche in einem umfassenden, gut strukturierten Bericht zusammenzufassen.
142133
Du erhältst eine oder mehrere Fragen und eine Liste von Analyseergebnissen. Daraus sollst du einen Recherchebericht und präzise, juristisch fundierte Antworten erarbeiten.
143134
@@ -172,8 +163,14 @@
172163
- Der Text endet mit dem Kapitel 3 Grundlagen und Quellen. Gib danach keine weiteren Kommentare oder Erklärungen ab.
173164
174165
Hier ist die Frage bzw. die Fragen des Experten:
166+
<expertenfrage>
175167
{user_query}
168+
</expertenfrage>
176169
177170
Hier sind die Analyseergebnisse von relevanten Dokumenten, die bisher erarbeitet wurden:
171+
<analyseergebnisse>
178172
{research_results}
179-
"""
173+
</analyseergebnisse>
174+
175+
Erstelle jetzt den Recherchebericht und die Antworten auf die Fragen des Experten.
176+
""".strip()

02_app/_core/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def get_model_and_workflow_config(fast_mode=False):
8282
"check_relevance": config["models"]["performance_low"],
8383
"analyze_documents": config["models"]["performance_medium"],
8484
"reflect_task": config["models"]["performance_medium"],
85-
"final_report": config["models"]["performance_medium"],
85+
"final_report": config["models"]["performance_high"],
8686
}
8787
workflow_config = {
8888
"max_queries": config["app"]["max_queries"],

0 commit comments

Comments
 (0)