Spaces:
Sleeping
Sleeping
Commit
·
5048ea5
1
Parent(s):
a42ab42
update to remove deprecation warning on launch
Browse files- app.py +56 -30
- helpers.py +20 -0
- 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[
|
|
|
|
|
|
|
|
|
|
|
26 |
user_msg = (user_msg or "").strip()
|
27 |
if not user_msg:
|
28 |
return "", history
|
29 |
-
|
30 |
-
|
31 |
-
|
|
|
|
|
|
|
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 |
-
|
|
|
38 |
"""
|
|
|
|
|
|
|
39 |
if not history:
|
40 |
-
yield history,
|
41 |
return
|
42 |
|
|
|
43 |
if api_key:
|
44 |
os.environ["OPENAI_API_KEY"] = api_key.strip()
|
45 |
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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[
|
54 |
-
yield history,
|
55 |
return
|
56 |
|
57 |
-
#
|
58 |
-
|
59 |
-
|
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 |
-
|
69 |
-
|
70 |
-
yield
|
71 |
except Exception as e:
|
72 |
-
history[
|
73 |
-
yield history,
|
74 |
return
|
75 |
|
76 |
# Finalize + linkify citations
|
77 |
acc_linked = linkify_text_with_sources(acc, hits[:k])
|
78 |
-
history[
|
79 |
|
80 |
# Decide whether to show sources
|
81 |
if is_unknown_answer(acc_linked):
|
82 |
# No sources for unknown / reformulate
|
83 |
-
yield history,
|
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 |
-
|
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
|
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=
|
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
|