julesbonnard commited on
Commit
5e1f3cc
·
1 Parent(s): 94a1d5e

works with sources

Browse files
Files changed (2) hide show
  1. README.md +6 -0
  2. app.py +399 -172
README.md CHANGED
@@ -9,3 +9,9 @@ app_file: app.py
9
  pinned: false
10
  ---
11
  AskNews-powered news assistant built with [Gradio](https://gradio.app), [`google-genai`](https://pypi.org/project/google-genai/), and the [AskNews SDK](https://pypi.org/project/asknews/).
 
 
 
 
 
 
 
9
  pinned: false
10
  ---
11
  AskNews-powered news assistant built with [Gradio](https://gradio.app), [`google-genai`](https://pypi.org/project/google-genai/), and the [AskNews SDK](https://pypi.org/project/asknews/).
12
+
13
+ ## Highlights
14
+ - Gemini responses streaming via `google-genai`.
15
+ - AskNews context injection configurable (`nl`, `kw`, `both`), optional source diversification, and language filters.
16
+ - Sidebar shows the fetched AskNews sources (titles, domains, dates, and links).
17
+ - Adjustable generation parameters (system prompt, max tokens, temperature, top-p, model name).
app.py CHANGED
@@ -3,7 +3,7 @@
3
  import os
4
  import datetime
5
  import logging
6
- from typing import List, Dict, Optional
7
 
8
  from dotenv import load_dotenv
9
  load_dotenv()
@@ -13,13 +13,52 @@ from google import genai
13
  from google.genai import types
14
  from asknews_sdk import AskNewsSDK
15
 
16
- DEFAULT_MODEL = "gemini-2.5-pro"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
 
19
  LOG_LEVEL = os.getenv("ASKNEWS_LOG_LEVEL", "INFO").upper()
20
  logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO))
21
  logger = logging.getLogger("asknews_app")
22
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  # ---- AskNews setup ----
24
  def get_asknews_sdk() -> Optional[AskNewsSDK]:
25
  """
@@ -43,158 +82,256 @@ def get_asknews_sdk() -> Optional[AskNewsSDK]:
43
  logger.exception("Failed to initialise AskNews SDK: %s", exc)
44
  return None
45
 
46
-
47
- def safe_iso_date(dt: Optional[str]) -> str:
48
- """Format date string safely for display."""
49
- if not dt:
50
- return ""
51
- try:
52
- # Attempt parsing common formats; if fails, return as-is
53
- # AskNews typically returns ISO timestamps
54
- d = datetime.datetime.fromisoformat(dt.replace("Z", "+00:00"))
55
- return d.strftime("%Y-%m-%d")
56
- except Exception:
57
- return dt
58
-
59
-
60
  def fetch_asknews_context(
61
  sdk: AskNewsSDK,
62
  query: str,
63
  hours_back: int,
64
  n_articles: int,
65
  domains: List[str],
66
- ) -> str:
 
 
 
67
  """
68
  Récupère le contexte texte directement depuis AskNews (return_type="string").
69
  Retourne context_text
70
  """
71
  logger.info(
72
- "Fetching AskNews context: query=%s, hours_back=%s, n_articles=%s, domains=%s",
73
  query,
74
  hours_back,
75
  n_articles,
76
  domains,
 
 
 
77
  )
78
  try:
79
- response = sdk.news.search_news(
80
- query=query,
81
- hours_back=hours_back,
82
- n_articles=n_articles,
83
- historical=True,
84
- premium=True,
85
- method="nl",
86
- domain_url=domains if domains else None,
87
- return_type="string", # Demande le contexte déjà formaté
88
- ).as_string
89
- # response est une chaîne de caractères contenant le contexte
90
- context = response if isinstance(response, str) else ""
91
- logger.info("AskNews context received (%s chars)", len(context))
92
- return context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  except Exception:
94
  logger.exception("AskNews context fetch failed.")
95
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
 
98
  # ---- Chat respond function ----
99
  def respond(
100
  message: str,
101
- history: List[Dict[str, str]],
102
  system_message: str,
103
  max_tokens: int,
104
  temperature: float,
105
  top_p: float,
106
- model_name: str = DEFAULT_MODEL,
107
- use_asknews: bool = True,
108
- asknews_hours_back: int = 24*30,
109
- asknews_n_articles: int = 10,
110
- asknews_domains_csv: str = "afp.com",
 
 
 
 
111
  ):
112
  """
113
  Stream chat responses from Google Gemini, enriching with AskNews context when enabled.
 
114
  """
115
- api_key = os.getenv("GOOGLE_API_KEY", "").strip()
 
 
 
 
 
 
 
 
 
116
  if not api_key:
117
- logger.warning("Missing Google API key.")
118
- yield (
119
  "Définissez GOOGLE_API_KEY dans votre environnement ou saisissez la clé API Google Gemini dans le champ dédié."
120
  )
 
 
 
121
  return
122
 
123
  try:
124
  genai_client = genai.Client(api_key=api_key)
125
  except Exception as exc:
126
  logger.exception("Failed to initialise Google GenAI client: %s", exc)
127
- yield f"Échec d'initialisation du client Google GenAI: {exc}"
 
 
128
  return
129
 
130
- user_message_raw = "" if message is None else str(message)
131
- user_message = user_message_raw.strip()
132
-
133
- # Prepare AskNews SDK if requested
134
- sdk = get_asknews_sdk() if use_asknews else None
135
- asknews_context = ""
136
- if sdk is not None:
137
- domains = [d.strip() for d in asknews_domains_csv.split(",") if d.strip()]
138
- logger.info(
139
- "AskNews enabled; fetching context with hours_back=%s, n_articles=%s, domains=%s",
140
- asknews_hours_back,
141
- asknews_n_articles,
142
- domains,
143
- )
144
- asknews_context = fetch_asknews_context(
145
- sdk=sdk,
146
- query=user_message,
147
- hours_back=asknews_hours_back,
148
- n_articles=asknews_n_articles,
149
- domains=domains,
150
- )
151
- if asknews_context:
152
- logger.info("AskNews context will be injected (chars=%s)", len(asknews_context))
153
  else:
154
- logger.warning("AskNews context is empty after fetch.")
155
-
156
- base_system = system_message.strip() if system_message else "You are a helpful assistant."
157
-
158
- if not user_message:
159
- yield "Veuillez saisir un message."
160
- return
161
-
162
- response_accum = ""
163
- # Optional prefix informing about context usage (not counted by model, only displayed)
164
- if sdk is None and use_asknews:
165
- response_accum = "[AskNews non configuré: définissez ASKNEWS_CLIENT_ID et ASKNEWS_CLIENT_SECRET dans l'environnement.]\n"
166
- yield response_accum
167
-
168
- # if sdk is not None:
169
- # context_display = asknews_context.strip()
170
- # if context_display:
171
- # if len(context_display) > 4000:
172
- # context_display = context_display[:4000] + "\n[Contexte AskNews tronqué pour affichage]"
173
- # else:
174
- # context_display = "[Vide]"
175
- # response_accum += "[Contexte AskNews]\n" + context_display + "\n\n"
176
- # yield response_accum
177
-
178
- system_instruction = base_system.strip()
179
- if asknews_context:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  system_instruction += (
181
  "\n\nUtilise le contexte AskNews suivant pour ta réponse. Si la question est sans rapport, ignore ce contexte.\n"
182
- f"{asknews_context}"
183
  )
184
 
185
  conversation: List[types.Content] = []
186
- for msg in history or []:
187
- role = msg.get("role")
188
- content = str(msg.get("content", "")).strip()
189
- if not content or role not in ("user", "assistant"):
190
- continue
191
- if role == "user":
192
  conversation.append(
193
- types.Content(role="user", parts=[types.Part.from_text(text=content)])
194
  )
195
- else:
196
  conversation.append(
197
- types.Content(role="model", parts=[types.Part.from_text(text=content)])
198
  )
199
 
200
  conversation.append(
@@ -208,91 +345,181 @@ def respond(
208
  maxOutputTokens=int(max_tokens),
209
  )
210
 
 
 
211
  try:
212
  stream = genai_client.models.generate_content_stream(
213
- model=model_name,
214
  contents=conversation,
215
  config=generation_config,
216
  )
217
  for chunk in stream:
218
- try:
219
- token = getattr(chunk, "text", None)
220
- if not token and getattr(chunk, "candidates", None):
221
- parts: List[str] = []
222
- for candidate in chunk.candidates:
223
- content = getattr(candidate, "content", None)
224
- if content and getattr(content, "parts", None):
225
- for part in content.parts:
226
- piece = getattr(part, "text", None)
227
- if piece:
228
- parts.append(piece)
229
- token = "".join(parts)
230
- if token:
231
- response_accum += token
232
- yield response_accum
233
- except Exception:
234
  continue
235
- except Exception as e:
236
- logger.exception("Google GenAI generation failed: %s", e)
237
- if response_accum:
238
- yield response_accum + f"\n\n[Erreur: {e}]"
239
- else:
240
- yield f"Erreur de génération Gemini: {e}"
241
 
 
 
 
 
 
 
 
 
 
242
 
243
- # ---- Gradio UI ----
244
- chatbot = gr.ChatInterface(
245
- fn=respond,
246
- type="messages",
247
- additional_inputs=[
248
- gr.Textbox(value="""Tu es un assistant virtuel conçu pour aider des journalistes d’agence (Agence France-Presse) dans leurs recherches d’information.
249
 
250
- Sources :
251
- - Tu disposes d’un agent de recherche en langage naturel (Asknews) qui interroge en temps réel le flux des dépêches AFP.
252
- - Tu dois répondre uniquement avec des informations issues de ces dépêches.
253
 
254
- Mission :
255
- - Comprendre les requêtes d’un journaliste (souvent courtes, imprécises, ou en langage naturel).
256
- - Transformer ces requêtes en recherches efficaces dans les dépêches AFP, avec Asknews.
257
- - Résumer les résultats en style journalistique : factuel, concis, hiérarchisé, neutre.
258
- - Proposer, si pertinent, des angles complémentaires (ex. contexte historique, réactions, comparaisons, chiffres clés).
259
- - Permettre au journaliste de raffiner la recherche (par période, sujet, acteurs, pays).
260
- - Citer les dépêches AFP en retour (référence et date/heure).
261
 
262
- Contraintes :
263
- - Toujours rester factuel, éviter toute spéculation.
264
- - Si la question est ambiguë, demander des précisions.
265
- - Si aucun résultat n’est trouvé, proposer des formulations alternatives de recherche.
266
- - Résumer les informations de manière actionnable (pour rédaction immédiate).
267
 
268
- Style :
269
- - Réponses brèves et efficaces.
270
- - Donner un résumé clair d’abord (les 2–3 points clés).
271
- - Ajouter ensuite plus de détails, ou des pistes pour approfondir.
272
- - Toujours indiquer les sources/dépêches AFP d’où viennent les infos.
273
- """, label="System message"),
274
- gr.Slider(minimum=1, maximum=4096, value=512, step=1, label="Max new tokens"),
275
- gr.Slider(minimum=0.0, maximum=2.0, value=0.7, step=0.1, label="Temperature"),
276
- gr.Slider(minimum=0.05, maximum=1.0, value=0.95, step=0.05, label="Top-p"),
277
- gr.Textbox(value=DEFAULT_MODEL, label="Model name"),
278
- gr.Checkbox(value=True, label="Utiliser AskNews pour le contexte"),
279
- gr.Slider(minimum=96, maximum=24*30, value=24*30, step=24, label="AskNews: heures en arrière"),
280
- gr.Slider(minimum=1, maximum=10, value=10, step=1, label="AskNews: nombre d'articles"),
281
- gr.Textbox(value="afp.com", label="AskNews: domaines (CSV)"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  ]
283
- )
284
 
285
- with gr.Blocks() as demo:
286
- gr.Markdown("# Chatbot Gemini avec contexte AskNews")
287
- with gr.Sidebar():
288
- gr.Markdown(
289
- "Définissez dans votre environnement les variables d'environnement suivantes :\n"
290
- "- ASKNEWS_CLIENT_ID\n"
291
- "- ASKNEWS_CLIENT_SECRET\n\n"
292
- "Configurer la clé Google Gemini via GOOGLE_API_KEY.\n\n"
293
- "Ajustez les paramètres pour contrôler le contexte (heures, domaines, nombre d'articles)."
294
- )
295
- chatbot.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  if __name__ == "__main__":
298
  demo.launch()
 
3
  import os
4
  import datetime
5
  import logging
6
+ from typing import List, Dict, Optional, Any, Tuple
7
 
8
  from dotenv import load_dotenv
9
  load_dotenv()
 
13
  from google.genai import types
14
  from asknews_sdk import AskNewsSDK
15
 
16
+ DEFAULT_MODEL = "gemini-2.0-flash"
17
+ DEFAULT_SYSTEM_PROMPT = """Tu es un assistant virtuel conçu pour aider des journalistes d’agence (Agence France-Presse) dans leurs recherches d’information.
18
+
19
+ Sources :
20
+ - Tu disposes d’un agent de recherche en langage naturel (Asknews) qui interroge en temps réel le flux des dépêches AFP.
21
+ - Tu dois répondre uniquement avec des informations issues de ces dépêches.
22
+
23
+ Mission :
24
+ - Comprendre les requêtes d’un journaliste (souvent courtes, imprécises, ou en langage naturel).
25
+ - Transformer ces requêtes en recherches efficaces dans les dépêches AFP, avec Asknews.
26
+ - Résumer les résultats en style journalistique : factuel, concis, hiérarchisé, neutre.
27
+ - Proposer, si pertinent, des angles complémentaires (ex. contexte historique, réactions, comparaisons, chiffres clés).
28
+ - Permettre au journaliste de raffiner la recherche (par période, sujet, acteurs, pays).
29
+ - Citer les dépêches AFP en retour (référence et date/heure).
30
+
31
+ Contraintes :
32
+ - Toujours rester factuel, éviter toute spéculation.
33
+ - Si la question est ambiguë, demander des précisions.
34
+ - Si aucun résultat n’est trouvé, proposer des formulations alternatives de recherche.
35
+ - Résumer les informations de manière actionnable (pour rédaction immédiate).
36
+
37
+ Style :
38
+ - Réponses brèves et efficaces.
39
+ - Donner un résumé clair d’abord (les 2–3 points clés).
40
+ - Ajouter ensuite plus de détails, ou des pistes pour approfondir.
41
+ - Toujours indiquer les sources/dépêches AFP d’où viennent les infos.
42
+ """
43
+ INITIAL_SOURCES_MARKDOWN = "*Aucune source pour l'instant.*"
44
 
45
 
46
  LOG_LEVEL = os.getenv("ASKNEWS_LOG_LEVEL", "INFO").upper()
47
  logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO))
48
  logger = logging.getLogger("asknews_app")
49
 
50
+ def format_pub_date(published):
51
+ if isinstance(published, datetime.datetime):
52
+ return published.strftime("%Y-%m-%d")
53
+ if isinstance(published, datetime.date):
54
+ return published.strftime("%Y-%m-%d")
55
+ if isinstance(published, str):
56
+ try:
57
+ return datetime.datetime.fromisoformat(published).strftime("%Y-%m-%d")
58
+ except ValueError:
59
+ return "unknown date"
60
+ return "unknown date"
61
+
62
  # ---- AskNews setup ----
63
  def get_asknews_sdk() -> Optional[AskNewsSDK]:
64
  """
 
82
  logger.exception("Failed to initialise AskNews SDK: %s", exc)
83
  return None
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def fetch_asknews_context(
86
  sdk: AskNewsSDK,
87
  query: str,
88
  hours_back: int,
89
  n_articles: int,
90
  domains: List[str],
91
+ method: str,
92
+ diversify_sources: bool,
93
+ languages: List[str],
94
+ ) -> Tuple[str, List[Dict[str, Any]]]:
95
  """
96
  Récupère le contexte texte directement depuis AskNews (return_type="string").
97
  Retourne context_text
98
  """
99
  logger.info(
100
+ "Fetching AskNews context: query=%s, hours_back=%s, n_articles=%s, domains=%s, method=%s, diversify=%s, languages=%s",
101
  query,
102
  hours_back,
103
  n_articles,
104
  domains,
105
+ method,
106
+ diversify_sources,
107
+ languages,
108
  )
109
  try:
110
+ kwargs: Dict[str, Any] = {
111
+ "query": query,
112
+ "hours_back": hours_back,
113
+ "n_articles": n_articles,
114
+ "historical": True,
115
+ "premium": True,
116
+ "method": method,
117
+ "domain_url": domains if domains else None,
118
+ "return_type": "both",
119
+ }
120
+ if diversify_sources:
121
+ kwargs["diversify_sources"] = True
122
+ if languages:
123
+ kwargs["languages"] = languages
124
+
125
+ response = sdk.news.search_news(**kwargs)
126
+ context_text = getattr(response, "as_string", "") or ""
127
+ raw_dicts = getattr(response, "as_dicts", None)
128
+ articles: List[Dict[str, Any]] = []
129
+ if isinstance(raw_dicts, list):
130
+ parsed_articles: List[Dict[str, Any]] = []
131
+ for item in raw_dicts:
132
+ if isinstance(item, dict):
133
+ parsed_articles.append(item)
134
+ continue
135
+
136
+ if hasattr(item, "model_dump"):
137
+ try:
138
+ data = item.model_dump(by_alias=True)
139
+ if isinstance(data, dict):
140
+ parsed_articles.append(data)
141
+ continue
142
+ except Exception:
143
+ logger.debug("model_dump(by_alias=True) failed for article", exc_info=True)
144
+
145
+ if hasattr(item, "dict"):
146
+ try:
147
+ data = item.dict(by_alias=True)
148
+ if isinstance(data, dict):
149
+ parsed_articles.append(data)
150
+ continue
151
+ except Exception:
152
+ logger.debug("dict(by_alias=True) failed for article", exc_info=True)
153
+
154
+ try:
155
+ parsed_articles.append(dict(item))
156
+ except Exception:
157
+ logger.debug("Fallback dict() conversion failed for article", exc_info=True)
158
+ articles = parsed_articles
159
+ logger.info(
160
+ "AskNews context received (%s chars, %s articles)",
161
+ len(context_text),
162
+ len(articles),
163
+ )
164
+ return context_text, articles
165
  except Exception:
166
  logger.exception("AskNews context fetch failed.")
167
+ return "", []
168
+
169
+
170
+ def parse_languages_csv(csv_input: str) -> List[str]:
171
+ return [lang.strip() for lang in csv_input.split(",") if lang.strip()]
172
+
173
+
174
+ def format_sources_markdown(articles: List[Dict[str, Any]]) -> str:
175
+ if not articles:
176
+ return "*Aucune source disponible pour cette requête.*"
177
+
178
+ lines: List[str] = []
179
+ for article in articles:
180
+ title = article.get("title")
181
+ source = article.get("markdown_citation")
182
+ key = article.get("as_string_key")
183
+ published = article.get("pub_date")
184
+
185
+ line = f"{key}. {format_pub_date(published)} - {title}"
186
+ if source:
187
+ line += f"\n {source}"
188
+ lines.append(line)
189
+
190
+ return "\n\n".join(lines)
191
 
192
 
193
  # ---- Chat respond function ----
194
  def respond(
195
  message: str,
196
+ history: Optional[List[Tuple[str, str]]],
197
  system_message: str,
198
  max_tokens: int,
199
  temperature: float,
200
  top_p: float,
201
+ model_name: str,
202
+ google_api_key: str,
203
+ use_asknews: bool,
204
+ asknews_hours_back: int,
205
+ asknews_n_articles: int,
206
+ asknews_domains_csv: str,
207
+ asknews_method: str,
208
+ asknews_diversify_sources: bool,
209
+ asknews_languages_csv: str,
210
  ):
211
  """
212
  Stream chat responses from Google Gemini, enriching with AskNews context when enabled.
213
+ Returns updates for both the chatbot conversation and the sources panel.
214
  """
215
+
216
+ conversation_history: List[Tuple[str, str]] = list(history or [])
217
+ user_message = (message or "").strip()
218
+
219
+ if not user_message:
220
+ logger.debug("Empty user message received.")
221
+ yield conversation_history, conversation_history, format_sources_markdown([])
222
+ return
223
+
224
+ api_key = (google_api_key or "").strip() or os.getenv("GOOGLE_API_KEY", "").strip()
225
  if not api_key:
226
+ warning = (
 
227
  "Définissez GOOGLE_API_KEY dans votre environnement ou saisissez la clé API Google Gemini dans le champ dédié."
228
  )
229
+ logger.warning("Missing Google API key.")
230
+ conversation_history.append((user_message, warning))
231
+ yield conversation_history, conversation_history, format_sources_markdown([])
232
  return
233
 
234
  try:
235
  genai_client = genai.Client(api_key=api_key)
236
  except Exception as exc:
237
  logger.exception("Failed to initialise Google GenAI client: %s", exc)
238
+ error_msg = f"Échec d'initialisation du client Google GenAI: {exc}"
239
+ conversation_history.append((user_message, error_msg))
240
+ yield conversation_history, conversation_history, format_sources_markdown([])
241
  return
242
 
243
+ domains = [d.strip() for d in (asknews_domains_csv or "").split(",") if d.strip()]
244
+ method = (asknews_method or "both").lower()
245
+ if method not in {"nl", "kw", "both"}:
246
+ method = "both"
247
+ languages = parse_languages_csv(asknews_languages_csv or "")
248
+ diversify_sources = bool(asknews_diversify_sources)
249
+
250
+ asknews_context_text = ""
251
+ asknews_articles: List[Dict[str, Any]] = []
252
+ asknews_notice = ""
253
+
254
+ if use_asknews:
255
+ sdk = get_asknews_sdk()
256
+ if sdk is None:
257
+ asknews_notice = (
258
+ "[AskNews non configuré: définissez ASKNEWS_CLIENT_ID et ASKNEWS_CLIENT_SECRET dans l'environnement.]"
259
+ )
260
+ logger.warning("AskNews SDK unavailable while use_asknews is True.")
 
 
 
 
 
261
  else:
262
+ asknews_context_text, asknews_articles = fetch_asknews_context(
263
+ sdk=sdk,
264
+ query=user_message,
265
+ hours_back=int(asknews_hours_back),
266
+ n_articles=int(asknews_n_articles),
267
+ domains=domains,
268
+ method=method,
269
+ diversify_sources=diversify_sources,
270
+ languages=languages,
271
+ )
272
+ if asknews_context_text:
273
+ logger.info(
274
+ "AskNews context ready (chars=%s, articles=%s)",
275
+ len(asknews_context_text),
276
+ len(asknews_articles),
277
+ )
278
+ else:
279
+ logger.warning("AskNews context is empty after fetch.")
280
+ else:
281
+ asknews_notice = "[AskNews désactivé pour cette requête.]"
282
+
283
+ if use_asknews:
284
+ if asknews_articles:
285
+ sources_markdown = format_sources_markdown(asknews_articles)
286
+ elif asknews_notice:
287
+ sources_markdown = asknews_notice + "\n\n" + INITIAL_SOURCES_MARKDOWN
288
+ else:
289
+ sources_markdown = format_sources_markdown([])
290
+ else:
291
+ sources_markdown = "*AskNews désactivé.*"
292
+
293
+ base_system = system_message.strip() if system_message else DEFAULT_SYSTEM_PROMPT
294
+ conversation_history.append((user_message, ""))
295
+
296
+ assistant_reply = ""
297
+ if asknews_notice:
298
+ assistant_reply += asknews_notice.strip()
299
+
300
+ # if asknews_context_text:
301
+ # context_display = asknews_context_text.strip()
302
+ # truncated = False
303
+ # if len(context_display) > 4000:
304
+ # context_display = context_display[:4000] + "\n[Contexte AskNews tronqué pour affichage]"
305
+ # truncated = True
306
+ # if assistant_reply:
307
+ # assistant_reply += "\n\n"
308
+ # assistant_reply += "[Contexte AskNews]\n" + (context_display or "[Vide]")
309
+ # if not truncated:
310
+ # assistant_reply += "\n"
311
+ # elif not assistant_reply and use_asknews:
312
+ # assistant_reply = "[Contexte AskNews introuvable pour cette requête.]"
313
+
314
+ conversation_history[-1] = (user_message, assistant_reply)
315
+ yield conversation_history, conversation_history, sources_markdown
316
+
317
+ system_instruction = base_system
318
+ if asknews_context_text:
319
  system_instruction += (
320
  "\n\nUtilise le contexte AskNews suivant pour ta réponse. Si la question est sans rapport, ignore ce contexte.\n"
321
+ f"{asknews_context_text}"
322
  )
323
 
324
  conversation: List[types.Content] = []
325
+ for past_user, past_assistant in conversation_history[:-1]:
326
+ past_user_clean = (past_user or "").strip()
327
+ past_assistant_clean = (past_assistant or "").strip()
328
+ if past_user_clean:
 
 
329
  conversation.append(
330
+ types.Content(role="user", parts=[types.Part.from_text(text=past_user_clean)])
331
  )
332
+ if past_assistant_clean:
333
  conversation.append(
334
+ types.Content(role="model", parts=[types.Part.from_text(text=past_assistant_clean)])
335
  )
336
 
337
  conversation.append(
 
345
  maxOutputTokens=int(max_tokens),
346
  )
347
 
348
+ assistant_full_reply = assistant_reply
349
+
350
  try:
351
  stream = genai_client.models.generate_content_stream(
352
+ model=(model_name or DEFAULT_MODEL).strip() or DEFAULT_MODEL,
353
  contents=conversation,
354
  config=generation_config,
355
  )
356
  for chunk in stream:
357
+ token = getattr(chunk, "text", None)
358
+ if not token and getattr(chunk, "candidates", None):
359
+ pieces: List[str] = []
360
+ for candidate in chunk.candidates:
361
+ content = getattr(candidate, "content", None)
362
+ if content and getattr(content, "parts", None):
363
+ for part in content.parts:
364
+ text_piece = getattr(part, "text", None)
365
+ if text_piece:
366
+ pieces.append(text_piece)
367
+ token = "".join(pieces)
368
+ if not token:
 
 
 
 
369
  continue
 
 
 
 
 
 
370
 
371
+ assistant_full_reply += token
372
+ conversation_history[-1] = (user_message, assistant_full_reply)
373
+ yield conversation_history, conversation_history, sources_markdown
374
+ except Exception as exc:
375
+ logger.exception("Google GenAI generation failed: %s", exc)
376
+ error_suffix = f"\n\n[Erreur: {exc}]"
377
+ assistant_full_reply = (assistant_full_reply or "") + error_suffix
378
+ conversation_history[-1] = (user_message, assistant_full_reply)
379
+ yield conversation_history, conversation_history, sources_markdown
380
 
 
 
 
 
 
 
381
 
382
+ def clear_conversation() -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]], str]:
383
+ """Reset the chat history and sources panel."""
384
+ return [], [], INITIAL_SOURCES_MARKDOWN
385
 
 
 
 
 
 
 
 
386
 
 
 
 
 
 
387
 
388
+ # ---- Gradio UI ----
389
+ with gr.Blocks(title="AskNews Gemini") as demo:
390
+ gr.Markdown("# Chatbot Gemini avec contexte AskNews")
391
+
392
+ chat_state = gr.State([])
393
+
394
+ with gr.Row():
395
+ with gr.Column(scale=3):
396
+ chatbot = gr.Chatbot(label="Conversation", height=520)
397
+ user_input = gr.Textbox(
398
+ label="Message",
399
+ placeholder="Saisissez votre requête journalistique...",
400
+ lines=3,
401
+ )
402
+ with gr.Row():
403
+ send_button = gr.Button("Envoyer", variant="primary")
404
+ clear_button = gr.Button("Effacer la conversation")
405
+
406
+ with gr.Accordion("Paramètres", open=False):
407
+ system_message_box = gr.Textbox(
408
+ value=DEFAULT_SYSTEM_PROMPT,
409
+ label="System message",
410
+ lines=20,
411
+ )
412
+ max_tokens_slider = gr.Slider(
413
+ minimum=1,
414
+ maximum=4096,
415
+ value=4096,
416
+ step=100,
417
+ label="Max new tokens",
418
+ )
419
+ temperature_slider = gr.Slider(
420
+ minimum=0.0,
421
+ maximum=2.0,
422
+ value=0.7,
423
+ step=0.1,
424
+ label="Temperature",
425
+ )
426
+ top_p_slider = gr.Slider(
427
+ minimum=0.05,
428
+ maximum=1.0,
429
+ value=0.95,
430
+ step=0.05,
431
+ label="Top-p",
432
+ )
433
+ model_name_box = gr.Textbox(value=DEFAULT_MODEL, label="Model name")
434
+ google_api_key_box = gr.Textbox(
435
+ value="",
436
+ label="Google API Key (optionnel)",
437
+ type="password",
438
+ )
439
+ use_asknews_checkbox = gr.Checkbox(
440
+ value=True,
441
+ label="Utiliser AskNews pour le contexte",
442
+ )
443
+ asknews_hours_slider = gr.Slider(
444
+ minimum=1,
445
+ maximum=24 * 30,
446
+ value=24 * 30,
447
+ step=1,
448
+ label="AskNews: heures en arrière",
449
+ )
450
+ asknews_articles_slider = gr.Slider(
451
+ minimum=1,
452
+ maximum=50,
453
+ value=10,
454
+ step=1,
455
+ label="AskNews: nombre d'articles",
456
+ )
457
+ asknews_domains_box = gr.Textbox(
458
+ value="afp.com",
459
+ label="AskNews: domaines (CSV)",
460
+ )
461
+ asknews_method_radio = gr.Radio(
462
+ choices=["both", "nl", "kw"],
463
+ value="both",
464
+ label="AskNews: méthode de recherche",
465
+ )
466
+ asknews_diversify_checkbox = gr.Checkbox(
467
+ value=False,
468
+ label="AskNews: diversifier les sources",
469
+ )
470
+ asknews_languages_box = gr.Textbox(
471
+ value="",
472
+ label="AskNews: langues (codes CSV)",
473
+ )
474
+
475
+ with gr.Column(scale=2):
476
+ gr.Markdown("### Sources AskNews")
477
+ sources_panel = gr.Markdown(INITIAL_SOURCES_MARKDOWN)
478
+
479
+ input_components = [
480
+ user_input,
481
+ chat_state,
482
+ system_message_box,
483
+ max_tokens_slider,
484
+ temperature_slider,
485
+ top_p_slider,
486
+ model_name_box,
487
+ google_api_key_box,
488
+ use_asknews_checkbox,
489
+ asknews_hours_slider,
490
+ asknews_articles_slider,
491
+ asknews_domains_box,
492
+ asknews_method_radio,
493
+ asknews_diversify_checkbox,
494
+ asknews_languages_box,
495
  ]
 
496
 
497
+ output_components = [chatbot, chat_state, sources_panel]
498
+
499
+ def _reset_input() -> str:
500
+ return ""
501
+
502
+ send_event = user_input.submit(
503
+ respond,
504
+ inputs=input_components,
505
+ outputs=output_components,
506
+ queue=True,
507
+ )
508
+ send_event.then(_reset_input, inputs=None, outputs=user_input)
509
+
510
+ button_event = send_button.click(
511
+ respond,
512
+ inputs=input_components,
513
+ outputs=output_components,
514
+ queue=True,
515
+ )
516
+ button_event.then(_reset_input, inputs=None, outputs=user_input)
517
+
518
+ clear_button.click(clear_conversation, None, output_components).then(
519
+ _reset_input, inputs=None, outputs=user_input
520
+ )
521
+
522
+ demo.queue()
523
 
524
  if __name__ == "__main__":
525
  demo.launch()