edouardfoussier commited on
Commit
5048ea5
·
1 Parent(s): a42ab42

update to remove deprecation warning on launch

Browse files
Files changed (3) hide show
  1. app.py +56 -30
  2. helpers.py +20 -0
  3. rag/synth.py +5 -3
app.py CHANGED
@@ -8,7 +8,7 @@ load_dotenv(override=True)
8
 
9
  from rag.retrieval import search, ensure_ready
10
  from rag.synth import synth_answer_stream
11
- from helpers import _extract_cited_indices, linkify_text_with_sources, _group_sources_md, is_unknown_answer
12
 
13
 
14
  # ---------- Warm-Up ----------
@@ -22,65 +22,83 @@ def _warmup():
22
 
23
 
24
  # ---------- Chat step 1: add user message ----------
25
- def add_user(user_msg: str, history: list[tuple]) -> tuple[str, list[tuple]]:
 
 
 
 
 
26
  user_msg = (user_msg or "").strip()
27
  if not user_msg:
28
  return "", history
29
- # append a placeholder assistant turn for streaming
30
- history = history + [(user_msg, "")]
31
- return "", history
 
 
 
32
 
33
 
34
  # ---------- Chat step 2: stream assistant answer ----------
35
- def bot(history: list[tuple], api_key: str, top_k: int, model_name: str):
36
  """
37
- Yields (history, sources_markdown) while streaming.
 
38
  """
 
 
 
39
  if not history:
40
- yield history, "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
41
  return
42
 
 
43
  if api_key:
44
  os.environ["OPENAI_API_KEY"] = api_key.strip()
45
 
46
- user_msg, _ = history[-1]
 
 
 
 
 
 
 
47
 
48
  # Retrieval
49
  k = int(max(top_k, 1))
50
  try:
51
  hits = search(user_msg, top_k=k)
52
  except Exception as e:
53
- history[-1] = (user_msg, f"❌ Retrieval error: {e}")
54
- yield history, "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
55
  return
56
 
57
- # sources_md = sources_markdown(hits[:k])
58
-
59
- # show a small “thinking” placeholder immediately
60
- history[-1] = (user_msg, "⏳ Synthèse en cours…")
61
- yield history, "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
62
 
63
  # Streaming LLM
64
  acc = ""
65
  try:
66
- for chunk in synth_answer_stream(user_msg, hits[:k], model=model_name):
67
  acc += chunk or ""
68
- step_hist = deepcopy(history)
69
- step_hist[-1] = (user_msg, acc)
70
- yield step_hist, "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
71
  except Exception as e:
72
- history[-1] = (user_msg, f"❌ Synthèse: {e}")
73
- yield history, "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
74
  return
75
 
76
  # Finalize + linkify citations
77
  acc_linked = linkify_text_with_sources(acc, hits[:k])
78
- history[-1] = (user_msg, acc_linked)
79
 
80
  # Decide whether to show sources
81
  if is_unknown_answer(acc_linked):
82
  # No sources for unknown / reformulate
83
- yield history, "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
84
  return
85
 
86
  # Construit la section sources à partir des citations réelles [n]
@@ -90,7 +108,6 @@ def bot(history: list[tuple], api_key: str, top_k: int, model_name: str):
90
  return
91
 
92
  grouped_sources = _group_sources_md(hits[:k], used)
93
-
94
  yield history, gr_update(visible=True, value=grouped_sources)
95
  # yield history, sources_md
96
 
@@ -113,16 +130,24 @@ with gr.Blocks(theme="soft", fill_height=True) as demo:
113
  model = gr.Dropdown(
114
  label="⚙️ OpenAI model",
115
  choices=[
 
 
 
116
  "gpt-4o-mini",
117
  "gpt-4o",
118
  "gpt-4.1-mini",
119
- "gpt-3.5-turbo"
120
  ],
121
  value="gpt-4o-mini"
122
  )
123
  topk = gr.Slider(1, 10, value=5, step=1, label="Top-K passages")
124
- # you can wire this later; not used now
125
-
 
 
 
 
 
126
  with gr.Row():
127
  with gr.Column(scale=4):
128
  chat = gr.Chatbot(
@@ -135,6 +160,7 @@ with gr.Blocks(theme="soft", fill_height=True) as demo:
135
  ),
136
  render_markdown=True,
137
  show_label=False,
 
138
  placeholder="<p style='text-align: center;'>Bonjour 👋,</p><p style='text-align: center;'>Je suis votre assistant HR. Je me tiens prêt à répondre à vos questions.</p>"
139
  )
140
  # input row
@@ -155,7 +181,7 @@ with gr.Blocks(theme="soft", fill_height=True) as demo:
155
  send_click = send.click(add_user, [msg, state], [msg, state])
156
  send_click.then(
157
  bot,
158
- [state, api_key, topk, model],
159
  [chat, sources],
160
  show_progress="minimal",
161
  ).then(lambda h: h, chat, state)
@@ -163,7 +189,7 @@ with gr.Blocks(theme="soft", fill_height=True) as demo:
163
  msg_submit = msg.submit(add_user, [msg, state], [msg, state])
164
  msg_submit.then(
165
  bot,
166
- [state, api_key, topk, model],
167
  [chat, sources],
168
  show_progress="minimal",
169
  ).then(lambda h: h, chat, state)
 
8
 
9
  from rag.retrieval import search, ensure_ready
10
  from rag.synth import synth_answer_stream
11
+ from helpers import _extract_cited_indices, linkify_text_with_sources, _group_sources_md, is_unknown_answer, _last_user_and_assistant_idxs
12
 
13
 
14
  # ---------- Warm-Up ----------
 
22
 
23
 
24
  # ---------- Chat step 1: add user message ----------
25
+ def add_user(user_msg: str, history: list[dict]):
26
+ """
27
+ history (messages mode) looks like:
28
+ [{"role":"user","content":"..."}, {"role":"assistant","content":"..."}, ...]
29
+ We append the user's message, then an empty assistant message to stream into.
30
+ """
31
  user_msg = (user_msg or "").strip()
32
  if not user_msg:
33
  return "", history
34
+
35
+ new_history = history + [
36
+ {"role": "user", "content": user_msg},
37
+ {"role": "assistant", "content": ""}, # placeholder for streaming
38
+ ]
39
+ return "", new_history
40
 
41
 
42
  # ---------- Chat step 2: stream assistant answer ----------
43
+ def bot(history: list[tuple], api_key: str, top_k: int, model_name: str, temperature: float):
44
  """
45
+ Streaming generator for messages-format history.
46
+ Yields (updated_history, sources_markdown).
47
  """
48
+ # Initial sources panel content
49
+ empty_sources = "### 📚 Sources\n_Ici, vous pourrez consulter les sources utilisées pour formuler la réponse._"
50
+
51
  if not history:
52
+ yield history, empty_sources
53
  return
54
 
55
+ # Inject BYO key if provided
56
  if api_key:
57
  os.environ["OPENAI_API_KEY"] = api_key.strip()
58
 
59
+ # Identify the pair (user -> assistant placeholder)
60
+ try:
61
+ u_idx, a_idx = _last_user_and_assistant_idxs(history)
62
+ except Exception:
63
+ yield history, empty_sources
64
+ return
65
+
66
+ user_msg = history[u_idx]["content"]
67
 
68
  # Retrieval
69
  k = int(max(top_k, 1))
70
  try:
71
  hits = search(user_msg, top_k=k)
72
  except Exception as e:
73
+ history[a_idx]["content"] = f"❌ Retrieval error: {e}"
74
+ yield history, empty_sources
75
  return
76
 
77
+ # Show a small “thinking” placeholder immediately
78
+ history[a_idx]["content"] = "⏳ Synthèse en cours…"
79
+ yield history, empty_sources
 
 
80
 
81
  # Streaming LLM
82
  acc = ""
83
  try:
84
+ for chunk in synth_answer_stream(user_msg, hits[:k], model=model_name, temperature=temperature):
85
  acc += chunk or ""
86
+ history[a_idx]["content"] = acc
87
+ # Stream without sources first (or keep a lightweight panel if you prefer)
88
+ yield history, empty_sources
89
  except Exception as e:
90
+ history[a_idx]["content"] = f"❌ Synthèse: {e}"
91
+ yield history, empty_sources
92
  return
93
 
94
  # Finalize + linkify citations
95
  acc_linked = linkify_text_with_sources(acc, hits[:k])
96
+ history[a_idx]["content"] = acc_linked
97
 
98
  # Decide whether to show sources
99
  if is_unknown_answer(acc_linked):
100
  # No sources for unknown / reformulate
101
+ yield history, empty_sources
102
  return
103
 
104
  # Construit la section sources à partir des citations réelles [n]
 
108
  return
109
 
110
  grouped_sources = _group_sources_md(hits[:k], used)
 
111
  yield history, gr_update(visible=True, value=grouped_sources)
112
  # yield history, sources_md
113
 
 
130
  model = gr.Dropdown(
131
  label="⚙️ OpenAI model",
132
  choices=[
133
+ "gpt-5",
134
+ "gpt-5-mini",
135
+ "gpt-5-nano",
136
  "gpt-4o-mini",
137
  "gpt-4o",
138
  "gpt-4.1-mini",
139
+ "gpt-3.5-turbo",
140
  ],
141
  value="gpt-4o-mini"
142
  )
143
  topk = gr.Slider(1, 10, value=5, step=1, label="Top-K passages")
144
+ temperature = gr.Slider(
145
+ minimum=0.0,
146
+ maximum=1.0,
147
+ value=0.2, # valeur par défaut
148
+ step=0.1,
149
+ label="Température du modèle"
150
+ )
151
  with gr.Row():
152
  with gr.Column(scale=4):
153
  chat = gr.Chatbot(
 
160
  ),
161
  render_markdown=True,
162
  show_label=False,
163
+ type="messages",
164
  placeholder="<p style='text-align: center;'>Bonjour 👋,</p><p style='text-align: center;'>Je suis votre assistant HR. Je me tiens prêt à répondre à vos questions.</p>"
165
  )
166
  # input row
 
181
  send_click = send.click(add_user, [msg, state], [msg, state])
182
  send_click.then(
183
  bot,
184
+ [state, api_key, topk, model, temperature],
185
  [chat, sources],
186
  show_progress="minimal",
187
  ).then(lambda h: h, chat, state)
 
189
  msg_submit = msg.submit(add_user, [msg, state], [msg, state])
190
  msg_submit.then(
191
  bot,
192
+ [state, api_key, topk, model, temperature],
193
  [chat, sources],
194
  show_progress="minimal",
195
  ).then(lambda h: h, chat, state)
helpers.py CHANGED
@@ -3,6 +3,26 @@ from collections import OrderedDict
3
 
4
  CITATION_RE = re.compile(r"\[(\d+)\]")
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  def is_unknown_answer(txt: str) -> bool:
8
  """Detect 'no answer' / 'reformulate' replies."""
 
3
 
4
  CITATION_RE = re.compile(r"\[(\d+)\]")
5
 
6
+ def _last_user_and_assistant_idxs(history: list[dict]) -> tuple[int, int]:
7
+ """
8
+ Find the last (user, assistant-placeholder) pair in messages history.
9
+ We expect the last message to be an assistant with empty content.
10
+ """
11
+ if not history:
12
+ raise ValueError("Empty history")
13
+ a_idx = len(history) - 1
14
+ if history[a_idx]["role"] != "assistant":
15
+ # be forgiving: fallback to creating one
16
+ history.append({"role": "assistant", "content": ""})
17
+ a_idx = len(history) - 1
18
+ # find the preceding user message
19
+ u_idx = a_idx - 1
20
+ while u_idx >= 0 and history[u_idx]["role"] != "user":
21
+ u_idx -= 1
22
+ if u_idx < 0:
23
+ raise ValueError("No preceding user message found")
24
+ return u_idx, a_idx
25
+
26
 
27
  def is_unknown_answer(txt: str) -> bool:
28
  """Detect 'no answer' / 'reformulate' replies."""
rag/synth.py CHANGED
@@ -54,18 +54,20 @@ def _build_prompt(query, passages):
54
  "Réponse:"
55
  )
56
 
57
- def synth_answer_stream(query, passages, model: str | None = None):
58
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=LLM_BASE_URL)
59
  model = model or LLM_MODEL
60
  prompt = utf8_safe(_build_prompt(query, passages))
 
61
 
62
  stream = client.chat.completions.create(
63
  model=LLM_MODEL,
64
  messages=[{"role": "user", "content": prompt}],
65
- temperature=0.2,
66
  stream=True,
67
  )
68
-
 
69
  for event in stream:
70
  if not getattr(event, "choices", None):
71
  continue
 
54
  "Réponse:"
55
  )
56
 
57
+ def synth_answer_stream(query, passages, model: str | None = None, temperature: float = 0.2):
58
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=LLM_BASE_URL)
59
  model = model or LLM_MODEL
60
  prompt = utf8_safe(_build_prompt(query, passages))
61
+ temperature = float(temperature)
62
 
63
  stream = client.chat.completions.create(
64
  model=LLM_MODEL,
65
  messages=[{"role": "user", "content": prompt}],
66
+ temperature=temperature,
67
  stream=True,
68
  )
69
+ # print(f"[synth] payload temperature={temperature}", flush=True)
70
+ # print(f"[synth] payload model={model}", flush=True)
71
  for event in stream:
72
  if not getattr(event, "choices", None):
73
  continue