Spaces:
Running
Running
Upload 5 files
Browse files- app.py +247 -0
- code (9).txt +7 -0
- config.py +87 -0
- rag_processor.py +219 -0
- utils.py +93 -0
app.py
ADDED
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import time
|
3 |
+
import asyncio
|
4 |
+
import nest_asyncio
|
5 |
+
import traceback
|
6 |
+
from typing import List, Dict, Any
|
7 |
+
import re # for extracting citation IDs
|
8 |
+
|
9 |
+
# --- Configuration and Service Initialization ---
|
10 |
+
try:
|
11 |
+
print("App: Loading config...")
|
12 |
+
import config
|
13 |
+
print("App: Loading utils...")
|
14 |
+
from utils import clean_source_text
|
15 |
+
print("App: Loading services...")
|
16 |
+
from services.retriever import init_retriever, get_retriever_status
|
17 |
+
from services.openai_service import init_openai_client, get_openai_status
|
18 |
+
print("App: Loading RAG processor... ")
|
19 |
+
from rag_processor import execute_validate_generate_pipeline, PIPELINE_VALIDATE_GENERATE_GPT4O
|
20 |
+
print("App: Imports successful.")
|
21 |
+
except ImportError as e:
|
22 |
+
st.error(f"Fatal Error: Module import failed. {e}", icon="🚨")
|
23 |
+
traceback.print_exc()
|
24 |
+
st.stop()
|
25 |
+
except Exception as e:
|
26 |
+
st.error(f"Fatal Error during initial setup: {e}", icon="🚨")
|
27 |
+
traceback.print_exc()
|
28 |
+
st.stop()
|
29 |
+
|
30 |
+
nest_asyncio.apply()
|
31 |
+
|
32 |
+
# --- Initialize Required Services ---
|
33 |
+
print("App: Initializing services...")
|
34 |
+
try:
|
35 |
+
retriever_ready_init, retriever_msg_init = init_retriever()
|
36 |
+
openai_ready_init, openai_msg_init = init_openai_client()
|
37 |
+
print("App: Service initialization calls complete.")
|
38 |
+
except Exception as init_err:
|
39 |
+
st.error(f"Error during service initialization: {init_err}", icon="🔥")
|
40 |
+
traceback.print_exc()
|
41 |
+
|
42 |
+
# --- Streamlit Page Configuration and Styling ---
|
43 |
+
st.set_page_config(page_title="Divrey Yoel AI Chat (GPT-4o Gen)", layout="wide")
|
44 |
+
st.markdown("""<style> /* ... Keep existing styles ... */ </style>""", unsafe_allow_html=True)
|
45 |
+
st.markdown("<h1 class='rtl-text'>דברות קודש - חיפוש ועיון</h1>", unsafe_allow_html=True)
|
46 |
+
st.markdown("<p class='rtl-text'>מבוסס על ספרי דברי יואל מסאטמאר זצוק'ל - אחזור מידע חכם (RAG)</p>", unsafe_allow_html=True)
|
47 |
+
st.markdown("<p class='rtl-text' style='font-size: 0.9em; color: #555;'>תהליך: אחזור → אימות (GPT-4o) → יצירה (GPT-4o)</p>", unsafe_allow_html=True)
|
48 |
+
|
49 |
+
# --- UI Helper Functions ---
|
50 |
+
def display_sidebar() -> Dict[str, Any]:
|
51 |
+
st.sidebar.markdown("<h3 class='rtl-text'>מצב המערכת</h3>", unsafe_allow_html=True)
|
52 |
+
retriever_ready, _ = get_retriever_status()
|
53 |
+
openai_ready, _ = get_openai_status()
|
54 |
+
st.sidebar.markdown(
|
55 |
+
f"<p class='rtl-text'><strong>מאחזר (Pinecone):</strong> {'✅' if retriever_ready else '❌'}</p>",
|
56 |
+
unsafe_allow_html=True
|
57 |
+
)
|
58 |
+
if not retriever_ready:
|
59 |
+
st.sidebar.error("מאחזר אינו זמין.", icon="🛑")
|
60 |
+
st.stop()
|
61 |
+
st.sidebar.markdown(
|
62 |
+
f"<p class='rtl-text'><strong>OpenAI ({config.OPENAI_VALIDATION_MODEL} / {config.OPENAI_GENERATION_MODEL}):</strong> {'✅' if openai_ready else '❌'}</p>",
|
63 |
+
unsafe_allow_html=True
|
64 |
+
)
|
65 |
+
if not openai_ready:
|
66 |
+
st.sidebar.error("OpenAI אינו זמין.", icon="⚠️")
|
67 |
+
st.sidebar.markdown("<hr>", unsafe_allow_html=True)
|
68 |
+
|
69 |
+
n_retrieve = st.sidebar.slider("מספר פסקאות לאחזור", 1, 300, config.DEFAULT_N_RETRIEVE)
|
70 |
+
max_validate = min(n_retrieve, 100)
|
71 |
+
n_validate = st.sidebar.slider(
|
72 |
+
"פסקאות לאימות (GPT-4o)",
|
73 |
+
1,
|
74 |
+
max_validate,
|
75 |
+
min(config.DEFAULT_N_VALIDATE, max_validate),
|
76 |
+
disabled=not openai_ready
|
77 |
+
)
|
78 |
+
st.sidebar.info("התשובות מבוססות רק על המקורות שאומתו.", icon="ℹ️")
|
79 |
+
|
80 |
+
# Prompt editors
|
81 |
+
with st.sidebar.expander("Edit RAG Prompts", expanded=False):
|
82 |
+
gen_prompt = st.text_area(
|
83 |
+
"System prompt (generation)",
|
84 |
+
value=config.OPENAI_SYSTEM_PROMPT,
|
85 |
+
height=200
|
86 |
+
)
|
87 |
+
val_prompt = st.text_area(
|
88 |
+
"Validation prompt (GPT-4o)",
|
89 |
+
value=config.VALIDATION_PROMPT_TEMPLATE,
|
90 |
+
height=200
|
91 |
+
)
|
92 |
+
config.OPENAI_SYSTEM_PROMPT = gen_prompt
|
93 |
+
config.VALIDATION_PROMPT_TEMPLATE = val_prompt
|
94 |
+
|
95 |
+
return {"n_retrieve": n_retrieve, "n_validate": n_validate, "services_ready": (retriever_ready and openai_ready)}
|
96 |
+
|
97 |
+
|
98 |
+
def display_chat_message(message: Dict[str, Any]):
|
99 |
+
role = message.get("role", "assistant")
|
100 |
+
with st.chat_message(role):
|
101 |
+
st.markdown(message.get('content', ''), unsafe_allow_html=True)
|
102 |
+
if role == "assistant" and message.get("final_docs"):
|
103 |
+
docs = message["final_docs"]
|
104 |
+
# Expander for all passed docs
|
105 |
+
exp_title = f"<span class='rtl-text'>הצג {len(docs)} קטעי מקור שנשלחו למחולל</span>"
|
106 |
+
with st.expander(exp_title, expanded=False):
|
107 |
+
st.markdown("<div dir='rtl' class='expander-content'>", unsafe_allow_html=True)
|
108 |
+
for i, doc in enumerate(docs, start=1):
|
109 |
+
source = doc.get('source_name', '') or 'מקור לא ידוע'
|
110 |
+
text = clean_source_text(doc.get('hebrew_text', ''))
|
111 |
+
st.markdown(
|
112 |
+
f"<div class='source-info rtl-text'><strong>מקור {i}:</strong> {source}</div>",
|
113 |
+
unsafe_allow_html=True
|
114 |
+
)
|
115 |
+
st.markdown(f"<div class='hebrew-text'>{text}</div>", unsafe_allow_html=True)
|
116 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
117 |
+
|
118 |
+
|
119 |
+
def display_status_updates(status_log: List[str]):
|
120 |
+
if status_log:
|
121 |
+
with st.expander("<span class='rtl-text'>הצג פרטי עיבוד</span>", expanded=False):
|
122 |
+
for u in status_log:
|
123 |
+
st.markdown(
|
124 |
+
f"<code class='status-update rtl-text'>- {u}</code>",
|
125 |
+
unsafe_allow_html=True
|
126 |
+
)
|
127 |
+
|
128 |
+
# --- Main Application Logic ---
|
129 |
+
if "messages" not in st.session_state:
|
130 |
+
st.session_state.messages = []
|
131 |
+
|
132 |
+
rag_params = display_sidebar()
|
133 |
+
|
134 |
+
# Render history
|
135 |
+
for msg in st.session_state.messages:
|
136 |
+
display_chat_message(msg)
|
137 |
+
|
138 |
+
if prompt := st.chat_input("שאל שאלה בענייני חסידות...", disabled=not rag_params["services_ready"]):
|
139 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
140 |
+
display_chat_message(st.session_state.messages[-1])
|
141 |
+
|
142 |
+
with st.chat_message("assistant"):
|
143 |
+
msg_placeholder = st.empty()
|
144 |
+
status_container = st.status("מעבד בקשה...", expanded=True)
|
145 |
+
chunks: List[str] = []
|
146 |
+
try:
|
147 |
+
def status_cb(m): status_container.update(label=f"<span class='rtl-text'>{m}</span>")
|
148 |
+
def stream_cb(c):
|
149 |
+
chunks.append(c)
|
150 |
+
msg_placeholder.markdown(
|
151 |
+
f"<div dir='rtl' class='rtl-text'>{''.join(chunks)}▌</div>",
|
152 |
+
unsafe_allow_html=True
|
153 |
+
)
|
154 |
+
|
155 |
+
loop = asyncio.get_event_loop()
|
156 |
+
final_rag = loop.run_until_complete(
|
157 |
+
execute_validate_generate_pipeline(
|
158 |
+
history=st.session_state.messages,
|
159 |
+
params=rag_params,
|
160 |
+
status_callback=status_cb,
|
161 |
+
stream_callback=stream_cb
|
162 |
+
)
|
163 |
+
)
|
164 |
+
|
165 |
+
if isinstance(final_rag, dict):
|
166 |
+
raw = final_rag.get("final_response", "")
|
167 |
+
err = final_rag.get("error")
|
168 |
+
log = final_rag.get("status_log", [])
|
169 |
+
docs = final_rag.get("generator_input_documents", [])
|
170 |
+
pipeline = final_rag.get("pipeline_used", PIPELINE_VALIDATE_GENERATE_GPT4O)
|
171 |
+
|
172 |
+
# wrap result
|
173 |
+
final = raw
|
174 |
+
if not (err and final.strip().startswith("<div")) and not final.strip().startswith((
|
175 |
+
'<div', '<p', '<ul', '<ol', '<strong'
|
176 |
+
)):
|
177 |
+
final = f"<div dir='rtl' class='rtl-text'>{final or 'לא התקבלה תשובה מהמחולל.'}</div>"
|
178 |
+
msg_placeholder.markdown(final, unsafe_allow_html=True)
|
179 |
+
|
180 |
+
# Identify cited IDs
|
181 |
+
cited_ids = set(re.findall(r'\(מקור\s*([0-9]+)\)', raw))
|
182 |
+
if cited_ids:
|
183 |
+
enumerated_docs = list(enumerate(docs, start=1))
|
184 |
+
docs_to_show = [(idx, doc) for idx, doc in enumerated_docs if str(idx) in cited_ids]
|
185 |
+
else:
|
186 |
+
docs_to_show = list(enumerate(docs, start=1))
|
187 |
+
|
188 |
+
if docs_to_show:
|
189 |
+
label = f"<span class='rtl-text'>הצג {len(docs_to_show)} קטעי מקור שהוזכרו בתשובה</span>"
|
190 |
+
with st.expander(label, expanded=False):
|
191 |
+
st.markdown("<div dir='rtl' class='expander-content'>", unsafe_allow_html=True)
|
192 |
+
for idx, doc in docs_to_show:
|
193 |
+
source = doc.get('source_name', '') or 'מקור לא ידוע'
|
194 |
+
text = clean_source_text(doc.get('hebrew_text', ''))
|
195 |
+
st.markdown(
|
196 |
+
f"<div class='source-info rtl-text'><strong>מקור {idx}:</strong> {source}</div>",
|
197 |
+
unsafe_allow_html=True
|
198 |
+
)
|
199 |
+
st.markdown(f"<div class='hebrew-text'>{text}</div>", unsafe_allow_html=True)
|
200 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
201 |
+
|
202 |
+
# store message
|
203 |
+
assistant_data = {
|
204 |
+
"role": "assistant",
|
205 |
+
"content": final,
|
206 |
+
"final_docs": docs,
|
207 |
+
"pipeline_used": pipeline,
|
208 |
+
"status_log": log,
|
209 |
+
"error": err
|
210 |
+
}
|
211 |
+
st.session_state.messages.append(assistant_data)
|
212 |
+
display_status_updates(log)
|
213 |
+
if err:
|
214 |
+
status_container.update(label="שגיאה בעיבוד!", state="error", expanded=False)
|
215 |
+
else:
|
216 |
+
status_container.update(label="העיבוד הושלם!", state="complete", expanded=False)
|
217 |
+
else:
|
218 |
+
msg_placeholder.markdown(
|
219 |
+
"<div dir='rtl' class='rtl-text'><strong>שגיאה בלתי צפויה בתקשורת.</strong></div>",
|
220 |
+
unsafe_allow_html=True
|
221 |
+
)
|
222 |
+
st.session_state.messages.append({
|
223 |
+
"role": "assistant",
|
224 |
+
"content": "שגיאה בלתי צפויה בתקשורת.",
|
225 |
+
"final_docs": [],
|
226 |
+
"pipeline_used": "Error",
|
227 |
+
"status_log": ["Unexpected result"],
|
228 |
+
"error": "Unexpected"
|
229 |
+
})
|
230 |
+
status_container.update(label="שגיאה בלתי צפויה!", state="error", expanded=False)
|
231 |
+
|
232 |
+
except Exception as e:
|
233 |
+
traceback.print_exc()
|
234 |
+
err_html = (
|
235 |
+
f"<div dir='rtl' class='rtl-text'><strong>שגיאה קריטית!</strong><br>נסה לרענן."
|
236 |
+
f"<details><summary>פרטים</summary><pre>{traceback.format_exc()}</pre></details></div>"
|
237 |
+
)
|
238 |
+
msg_placeholder.error(err_html, icon="🔥")
|
239 |
+
st.session_state.messages.append({
|
240 |
+
"role": "assistant",
|
241 |
+
"content": err_html,
|
242 |
+
"final_docs": [],
|
243 |
+
"pipeline_used": "Critical Error",
|
244 |
+
"status_log": [f"Critical: {type(e).__name__}"],
|
245 |
+
"error": str(e)
|
246 |
+
})
|
247 |
+
status_container.update(label=str(e), state="error", expanded=False)
|
code (9).txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# requirements.txt
|
2 |
+
streamlit
|
3 |
+
pinecone-client # Correct package name
|
4 |
+
openai
|
5 |
+
langsmith
|
6 |
+
nest_asyncio
|
7 |
+
python-dotenv # Optional, but harmless
|
config.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
|
4 |
+
load_dotenv()
|
5 |
+
|
6 |
+
# --- LangSmith Configuration ---
|
7 |
+
LANGSMITH_ENDPOINT = os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
|
8 |
+
LANGSMITH_TRACING = os.environ.get("LANGSMITH_TRACING", "true")
|
9 |
+
LANGSMITH_API_KEY = os.environ.get("LANGSMITH_API_KEY")
|
10 |
+
LANGSMITH_PROJECT = os.environ.get("LANGSMITH_PROJECT", "DivreyYoel-RAG-GPT4-Gen")
|
11 |
+
|
12 |
+
# --- API Keys (Required) ---
|
13 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
14 |
+
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
|
15 |
+
|
16 |
+
# --- Model Configuration ---
|
17 |
+
EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-large")
|
18 |
+
OPENAI_VALIDATION_MODEL = os.environ.get("OPENAI_VALIDATION_MODEL", "gpt-4o")
|
19 |
+
OPENAI_GENERATION_MODEL = os.environ.get("OPENAI_GENERATION_MODEL", "o3")
|
20 |
+
|
21 |
+
# --- Pinecone Configuration ---
|
22 |
+
PINECONE_INDEX_NAME = os.environ.get("PINECONE_INDEX_NAME", "chassidus-index")
|
23 |
+
|
24 |
+
# --- Default RAG Pipeline Parameters ---
|
25 |
+
DEFAULT_N_RETRIEVE = 300
|
26 |
+
DEFAULT_N_VALIDATE = 100
|
27 |
+
|
28 |
+
# --- System Prompts ---
|
29 |
+
OPENAI_SYSTEM_PROMPT = """You are an expert assistant specializing in Chassidic texts, particularly the works of the Satmar Rebbe, Rabbi Yoel Teitelbaum (Divrei Yoel).
|
30 |
+
Your task is to answer the user's question based *exclusively* on the provided source text snippets (paragraphs from relevant books). Do not use any prior knowledge or external information.
|
31 |
+
|
32 |
+
**Source Text Format:**
|
33 |
+
The relevant source texts will be provided below under the heading "Source Texts:". Each source is numbered and includes an ID.
|
34 |
+
|
35 |
+
**Response Requirements:**
|
36 |
+
1. **Language:** Respond **exclusively in Hebrew**.
|
37 |
+
2. **Basis:** Base your answer *strictly* on the information contained within the provided "Source Texts:". Do not infer, add external knowledge, or answer if the context does not contain relevant information.
|
38 |
+
3. **Attribution (Optional but Recommended):** When possible, mention the source number (e.g., "כפי שמופיע במקור 3") where the information comes from. Do not invent information. Use quotes sparingly and only when essential, quoting the Hebrew text directly.
|
39 |
+
4. **Completeness:** Synthesize information from *multiple* relevant sources if they contribute to the answer.
|
40 |
+
5. **Handling Lack of Information:** If the provided sources do not contain information relevant to the question, state clearly in Hebrew that the provided texts do not contain the answer (e.g., "על פי המקורות שסופקו, אין מידע לענות על שאלה זו."). Do not attempt to answer based on outside knowledge.
|
41 |
+
6. **Clarity and Conciseness:** Provide a clear, well-structured, and concise answer in Hebrew. Focus on directly answering the user's question.
|
42 |
+
7. **Tone:** Maintain a formal and respectful tone appropriate for discussing religious texts.
|
43 |
+
8. **No Greetings/Closings:** Do not include introductory greetings (e.g., "שלום") or concluding remarks (e.g., "בברכה", "מקווה שעזרתי"). Focus solely on the answer.
|
44 |
+
"""
|
45 |
+
|
46 |
+
VALIDATION_PROMPT_TEMPLATE = """
|
47 |
+
User Question (Hebrew):
|
48 |
+
\"{user_question}\"
|
49 |
+
|
50 |
+
Text Paragraph (Paragraph {paragraph_index}):
|
51 |
+
Hebrew:
|
52 |
+
---
|
53 |
+
{hebrew_text}
|
54 |
+
---
|
55 |
+
English:
|
56 |
+
---
|
57 |
+
{english_text}
|
58 |
+
---
|
59 |
+
|
60 |
+
Instruction:
|
61 |
+
Analyze the Text Paragraph. Determine if it contains information that *directly* answers or significantly contributes to answering the User Question.
|
62 |
+
Respond ONLY with valid JSON: {{\"contains_relevant_info\": boolean, \"justification\": \"Brief Hebrew explanation\"}}.
|
63 |
+
Output only the JSON object.
|
64 |
+
"""
|
65 |
+
|
66 |
+
# --- Helper Functions ---
|
67 |
+
def check_env_vars():
|
68 |
+
missing_keys = []
|
69 |
+
if not LANGSMITH_API_KEY: missing_keys.append("LANGSMITH_API_KEY")
|
70 |
+
if not OPENAI_API_KEY: missing_keys.append("OPENAI_API_KEY")
|
71 |
+
if not PINECONE_API_KEY: missing_keys.append("PINECONE_API_KEY")
|
72 |
+
return missing_keys
|
73 |
+
|
74 |
+
def configure_langsmith():
|
75 |
+
os.environ["LANGSMITH_ENDPOINT"] = LANGSMITH_ENDPOINT
|
76 |
+
os.environ["LANGSMITH_TRACING"] = LANGSMITH_TRACING
|
77 |
+
if LANGSMITH_API_KEY: os.environ["LANGSMITH_API_KEY"] = LANGSMITH_API_KEY
|
78 |
+
if LANGSMITH_PROJECT: os.environ["LANGSMITH_PROJECT"] = LANGSMITH_PROJECT
|
79 |
+
print(f"LangSmith configured: Endpoint={LANGSMITH_ENDPOINT}, Tracing={LANGSMITH_TRACING}, Project={LANGSMITH_PROJECT or 'Default'}")
|
80 |
+
|
81 |
+
missing = check_env_vars()
|
82 |
+
if missing:
|
83 |
+
print(f"Warning: Missing essential API keys: {', '.join(missing)}")
|
84 |
+
else:
|
85 |
+
print("All essential API keys found.")
|
86 |
+
|
87 |
+
configure_langsmith()
|
rag_processor.py
ADDED
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import time
|
2 |
+
import asyncio
|
3 |
+
import traceback
|
4 |
+
from typing import List, Dict, Any, Optional, Callable, Tuple
|
5 |
+
from langsmith import traceable
|
6 |
+
|
7 |
+
try:
|
8 |
+
import config
|
9 |
+
from services import retriever, openai_service
|
10 |
+
except ImportError:
|
11 |
+
print("Error: Failed to import config or services in rag_processor.py")
|
12 |
+
raise SystemExit("Failed imports in rag_processor.py")
|
13 |
+
|
14 |
+
PIPELINE_VALIDATE_GENERATE_GPT4O = "GPT-4o Validator + GPT-4o Synthesizer"
|
15 |
+
StatusCallback = Callable[[str], None]
|
16 |
+
|
17 |
+
# --- Step Functions ---
|
18 |
+
|
19 |
+
@traceable(name="rag-step-retrieve")
|
20 |
+
async def run_retrieval_step(query: str, n_retrieve: int, update_status: StatusCallback) -> List[Dict]:
|
21 |
+
update_status(f"1. מאחזר עד {n_retrieve} פסקאות מ-Pinecone...")
|
22 |
+
start_time = time.time()
|
23 |
+
retrieved_docs = retriever.retrieve_documents(query_text=query, n_results=n_retrieve)
|
24 |
+
retrieval_time = time.time() - start_time
|
25 |
+
status_msg = f"אוחזרו {len(retrieved_docs)} פסקאות ב-{retrieval_time:.2f} שניות."
|
26 |
+
update_status(f"1. {status_msg}")
|
27 |
+
if not retrieved_docs:
|
28 |
+
update_status("1. לא אותרו מסמכים.")
|
29 |
+
return retrieved_docs
|
30 |
+
|
31 |
+
@traceable(name="rag-step-gpt4o-filter")
|
32 |
+
async def run_gpt4o_validation_filter_step(
|
33 |
+
docs_to_process: List[Dict], query: str, n_validate: int, update_status: StatusCallback
|
34 |
+
) -> List[Dict]:
|
35 |
+
if not docs_to_process:
|
36 |
+
update_status("2. [GPT-4o] דילוג על אימות - אין פסקאות.")
|
37 |
+
return []
|
38 |
+
validation_count = min(len(docs_to_process), n_validate)
|
39 |
+
update_status(f"2. [GPT-4o] מתחיל אימות מקבילי ({validation_count} / {len(docs_to_process)} פסקאות)...")
|
40 |
+
validation_start_time = time.time()
|
41 |
+
tasks = [openai_service.validate_relevance_openai(doc, query, i)
|
42 |
+
for i, doc in enumerate(docs_to_process[:validation_count])]
|
43 |
+
validation_results = await asyncio.gather(*tasks, return_exceptions=True)
|
44 |
+
passed_docs = []
|
45 |
+
passed_count = failed_validation_count = error_count = 0
|
46 |
+
update_status("3. [GPT-4o] סינון פסקאות לפי תוצאות אימות...")
|
47 |
+
for i, res in enumerate(validation_results):
|
48 |
+
original_doc = docs_to_process[i]
|
49 |
+
if isinstance(res, Exception):
|
50 |
+
print(f"GPT-4o Validation Exception doc {i}: {res}")
|
51 |
+
error_count += 1
|
52 |
+
elif isinstance(res, dict) and 'validation' in res:
|
53 |
+
if res['validation'].get('contains_relevant_info'):
|
54 |
+
original_doc['validation_result'] = res['validation']
|
55 |
+
passed_docs.append(original_doc)
|
56 |
+
passed_count += 1
|
57 |
+
else:
|
58 |
+
failed_validation_count += 1
|
59 |
+
else:
|
60 |
+
print(f"GPT-4o Validation Unexpected result doc {i}: {type(res)}")
|
61 |
+
error_count += 1
|
62 |
+
validation_time = time.time() - validation_start_time
|
63 |
+
status_msg_val = (f"אימות GPT-4o הושלם ({passed_count} עברו, "
|
64 |
+
f"{failed_validation_count} נדחו, {error_count} שגיאות) "
|
65 |
+
f"ב-{validation_time:.2f} שניות.")
|
66 |
+
update_status(f"2. {status_msg_val}")
|
67 |
+
status_msg_filter = f"נאספו {len(passed_docs)} פסקאות רלוונטיות לאחר אימות GPT-4o."
|
68 |
+
update_status(f"3. {status_msg_filter}")
|
69 |
+
return passed_docs
|
70 |
+
|
71 |
+
@traceable(name="rag-step-openai-generate")
|
72 |
+
async def run_openai_generation_step(
|
73 |
+
history: List[Dict], context_documents: List[Dict],
|
74 |
+
update_status: StatusCallback, stream_callback: Callable[[str], None]
|
75 |
+
) -> Tuple[str, Optional[str]]:
|
76 |
+
generator_name = "OpenAI"
|
77 |
+
if not context_documents:
|
78 |
+
update_status(f"4. [{generator_name}] דילוג על יצירה - אין פסקאות להקשר.")
|
79 |
+
return "לא סופקו פסקאות רלוונטיות ליצירת התשובה.", None
|
80 |
+
update_status(f"4. [{generator_name}] מחולל תשובה סופית מ-{len(context_documents)} קטעי הקשר...")
|
81 |
+
start_gen_time = time.time()
|
82 |
+
try:
|
83 |
+
full_response = []
|
84 |
+
error_msg = None
|
85 |
+
generator = openai_service.generate_openai_stream(
|
86 |
+
messages=history, context_documents=context_documents
|
87 |
+
)
|
88 |
+
async for chunk in generator:
|
89 |
+
if isinstance(chunk, str) and chunk.strip().startswith("--- Error:"):
|
90 |
+
if not error_msg:
|
91 |
+
error_msg = chunk.strip()
|
92 |
+
print(f"OpenAI stream yielded error: {chunk.strip()}")
|
93 |
+
break
|
94 |
+
if isinstance(chunk, str):
|
95 |
+
full_response.append(chunk)
|
96 |
+
stream_callback(chunk)
|
97 |
+
final_response_text = "".join(full_response)
|
98 |
+
gen_time = time.time() - start_gen_time
|
99 |
+
if error_msg:
|
100 |
+
update_status(f"4. שגיאה ביצירת התשובה ({generator_name}) ב-{gen_time:.2f} שניות.")
|
101 |
+
return final_response_text, error_msg
|
102 |
+
update_status(f"4. יצירת התשובה ({generator_name}) הושלמה ב-{gen_time:.2f} שניות.")
|
103 |
+
return final_response_text, None
|
104 |
+
except Exception as gen_err:
|
105 |
+
gen_time = time.time() - start_gen_time
|
106 |
+
error_msg_critical = (f"--- Error: Critical failure during {generator_name} generation "
|
107 |
+
f"({type(gen_err).__name__}): {gen_err} ---")
|
108 |
+
update_status(f"4. שגיאה קריטית ביצירת התשובה ({generator_name}) ב-{gen_time:.2f} שניות.")
|
109 |
+
traceback.print_exc()
|
110 |
+
return "", error_msg_critical
|
111 |
+
|
112 |
+
@traceable(name="rag-execute-validate-generate-gpt4o-pipeline")
|
113 |
+
async def execute_validate_generate_pipeline(
|
114 |
+
history: List[Dict], params: Dict[str, Any],
|
115 |
+
status_callback: StatusCallback, stream_callback: Callable[[str], None]
|
116 |
+
) -> Dict[str, Any]:
|
117 |
+
result: Dict[str, Any] = {
|
118 |
+
"final_response": "",
|
119 |
+
"validated_documents_full": [],
|
120 |
+
"generator_input_documents": [],
|
121 |
+
"status_log": [],
|
122 |
+
"error": None,
|
123 |
+
"pipeline_used": PIPELINE_VALIDATE_GENERATE_GPT4O
|
124 |
+
}
|
125 |
+
status_log_internal: List[str] = []
|
126 |
+
|
127 |
+
def update_status_and_log(message: str):
|
128 |
+
print(f"Status Update: {message}")
|
129 |
+
status_log_internal.append(message)
|
130 |
+
status_callback(message)
|
131 |
+
|
132 |
+
current_query_text = ""
|
133 |
+
if history and isinstance(history, list):
|
134 |
+
for msg_ in reversed(history):
|
135 |
+
if isinstance(msg_, dict) and msg_.get("role") == "user":
|
136 |
+
current_query_text = str(msg_.get("content") or "")
|
137 |
+
break
|
138 |
+
if not current_query_text:
|
139 |
+
result["error"] = "לא זוהתה שאלה."
|
140 |
+
result["final_response"] = f"<div class='rtl-text'>{result['error']}</div>"
|
141 |
+
result["status_log"] = status_log_internal
|
142 |
+
return result
|
143 |
+
|
144 |
+
try:
|
145 |
+
# 1. Retrieval
|
146 |
+
retrieved_docs = await run_retrieval_step(
|
147 |
+
current_query_text, params['n_retrieve'], update_status_and_log
|
148 |
+
)
|
149 |
+
if not retrieved_docs:
|
150 |
+
result["error"] = "לא אותרו מקורות."
|
151 |
+
result["final_response"] = f"<div class='rtl-text'>{result['error']}</div>"
|
152 |
+
result["status_log"] = status_log_internal
|
153 |
+
return result
|
154 |
+
|
155 |
+
# 2. Validation
|
156 |
+
validated_docs_full = await run_gpt4o_validation_filter_step(
|
157 |
+
retrieved_docs, current_query_text, params['n_validate'], update_status_and_log
|
158 |
+
)
|
159 |
+
result["validated_documents_full"] = validated_docs_full
|
160 |
+
if not validated_docs_full:
|
161 |
+
result["error"] = "לא נמצאו פסקאות רלוונטיות."
|
162 |
+
result["final_response"] = f"<div class='rtl-text'>{result['error']}</div>"
|
163 |
+
update_status_and_log(f"4. {result['error']} לא ניתן להמשיך.")
|
164 |
+
return result
|
165 |
+
|
166 |
+
# --- Simplify Docs for Generation ---
|
167 |
+
simplified_docs_for_generation: List[Dict[str, Any]] = []
|
168 |
+
print(f"Processor: Simplifying {len(validated_docs_full)} docs...")
|
169 |
+
for doc in validated_docs_full:
|
170 |
+
if isinstance(doc, dict):
|
171 |
+
hebrew_text = doc.get('hebrew_text', '')
|
172 |
+
validation = doc.get('validation_result')
|
173 |
+
if hebrew_text:
|
174 |
+
simplified_doc: Dict[str, Any] = {
|
175 |
+
'hebrew_text': hebrew_text,
|
176 |
+
'original_id': doc.get('original_id', 'unknown')
|
177 |
+
}
|
178 |
+
if doc.get('source_name'):
|
179 |
+
simplified_doc['source_name'] = doc.get('source_name')
|
180 |
+
if validation is not None:
|
181 |
+
simplified_doc['validation_result'] = validation # include judgment
|
182 |
+
simplified_docs_for_generation.append(simplified_doc)
|
183 |
+
else:
|
184 |
+
print(f"Warn: Skipping non-dict item: {doc}")
|
185 |
+
result["generator_input_documents"] = simplified_docs_for_generation
|
186 |
+
print(f"Processor: Created {len(simplified_docs_for_generation)} simplified docs with validation results.")
|
187 |
+
|
188 |
+
# 3. Generation
|
189 |
+
final_response_text, generation_error = await run_openai_generation_step(
|
190 |
+
history=history,
|
191 |
+
context_documents=simplified_docs_for_generation,
|
192 |
+
update_status=update_status_and_log,
|
193 |
+
stream_callback=stream_callback
|
194 |
+
)
|
195 |
+
result["final_response"] = final_response_text
|
196 |
+
result["error"] = generation_error
|
197 |
+
|
198 |
+
if generation_error and not result["final_response"].strip().startswith(("<div", "לא סופקו")):
|
199 |
+
result["final_response"] = (
|
200 |
+
f"<div class='rtl-text'><strong>שגיאה ביצירת התשובה.</strong><br>"
|
201 |
+
f"פרטים: {generation_error}<br>---<br>{result['final_response']}</div>"
|
202 |
+
)
|
203 |
+
elif result["final_response"] == "לא סופקו פסקאות רלוונטיות ליצירת התשובה.":
|
204 |
+
result["final_response"] = f"<div class='rtl-text'>{result['final_response']}</div>"
|
205 |
+
|
206 |
+
except Exception as e:
|
207 |
+
error_type = type(e).__name__
|
208 |
+
error_msg = f"שגיאה קריטית RAG ({error_type}): {e}"
|
209 |
+
print(f"Critical RAG Error: {error_msg}")
|
210 |
+
traceback.print_exc()
|
211 |
+
result["error"] = error_msg
|
212 |
+
result["final_response"] = (
|
213 |
+
f"<div class='rtl-text'><strong>שגיאה קריטית! ({error_type})</strong><br>נסה שוב."
|
214 |
+
f"<details><summary>פרטים</summary><pre>{traceback.format_exc()}</pre></details></div>"
|
215 |
+
)
|
216 |
+
update_status_and_log(f"שגיאה קריטית: {error_type}")
|
217 |
+
|
218 |
+
result["status_log"] = status_log_internal
|
219 |
+
return result
|
utils.py
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# utils.py (Updated for OpenAI context formatting)
|
2 |
+
import re
|
3 |
+
import os
|
4 |
+
import time
|
5 |
+
import traceback
|
6 |
+
import openai
|
7 |
+
from typing import Optional, List, Dict
|
8 |
+
|
9 |
+
try:
|
10 |
+
import config
|
11 |
+
except ImportError:
|
12 |
+
print("Error: config.py not found. Cannot proceed.")
|
13 |
+
raise SystemExit("config.py not found")
|
14 |
+
|
15 |
+
# ... (keep openai_client init, clean_source_text, get_embedding) ...
|
16 |
+
openai_client = None
|
17 |
+
if config.OPENAI_API_KEY:
|
18 |
+
try:
|
19 |
+
openai_client = openai.OpenAI(api_key=config.OPENAI_API_KEY)
|
20 |
+
print("Utils: OpenAI client initialized for embeddings.")
|
21 |
+
except Exception as e:
|
22 |
+
print(f"Utils: Error initializing OpenAI client for embeddings: {e}")
|
23 |
+
else:
|
24 |
+
print("Utils: Warning - OPENAI_API_KEY not found. Embeddings will fail.")
|
25 |
+
|
26 |
+
def clean_source_text(text: Optional[str]) -> str:
|
27 |
+
if not text: return ""
|
28 |
+
text = text.replace('\x00', '').replace('\ufffd', '')
|
29 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
30 |
+
return text
|
31 |
+
|
32 |
+
def get_embedding(text: str, model: str = config.EMBEDDING_MODEL, max_retries: int = 3) -> Optional[List[float]]:
|
33 |
+
global openai_client
|
34 |
+
if not openai_client:
|
35 |
+
print("Error: OpenAI client not initialized (utils.py). Cannot get embedding.")
|
36 |
+
return None
|
37 |
+
if not text or not isinstance(text, str):
|
38 |
+
print("Error: Invalid input text for embedding.")
|
39 |
+
return None
|
40 |
+
cleaned_text = text.replace("\n", " ").strip()
|
41 |
+
if not cleaned_text:
|
42 |
+
print("Warning: Text is empty after cleaning, cannot get embedding.")
|
43 |
+
return None
|
44 |
+
attempt = 0
|
45 |
+
while attempt < max_retries:
|
46 |
+
try:
|
47 |
+
response = openai_client.embeddings.create(input=[cleaned_text], model=model)
|
48 |
+
return response.data[0].embedding
|
49 |
+
except openai.RateLimitError as e:
|
50 |
+
wait_time = (2 ** attempt); print(f"Rate limit embedding. Retrying in {wait_time}s..."); time.sleep(wait_time)
|
51 |
+
attempt += 1
|
52 |
+
except openai.APIConnectionError as e:
|
53 |
+
print(f"Connection error embedding. Retrying..."); time.sleep(2)
|
54 |
+
attempt += 1
|
55 |
+
except Exception as e:
|
56 |
+
print(f"Error generating embedding (Attempt {attempt + 1}/{max_retries}): {type(e).__name__}")
|
57 |
+
attempt += 1
|
58 |
+
print(f"Failed embedding after {max_retries} attempts.")
|
59 |
+
return None
|
60 |
+
|
61 |
+
# --- REMOVED format_context_for_anthropic ---
|
62 |
+
|
63 |
+
# --- NEW Function to format context for OpenAI ---
|
64 |
+
def format_context_for_openai(documents: List[Dict]) -> str:
|
65 |
+
"""Formats documents for the OpenAI prompt context section using numbered list."""
|
66 |
+
if not documents:
|
67 |
+
return "No source texts provided."
|
68 |
+
formatted_docs = []
|
69 |
+
language_key = 'hebrew_text'
|
70 |
+
id_key = 'original_id'
|
71 |
+
source_key = 'source_name' # Optional: Include source name if available
|
72 |
+
|
73 |
+
for index, doc in enumerate(documents):
|
74 |
+
if not isinstance(doc, dict):
|
75 |
+
print(f"Warning: Skipping non-dict item in documents list: {doc}")
|
76 |
+
continue
|
77 |
+
|
78 |
+
text = clean_source_text(doc.get(language_key, ''))
|
79 |
+
doc_id = doc.get(id_key, f'unknown_{index+1}')
|
80 |
+
source_name = doc.get(source_key, '') # Get source name
|
81 |
+
|
82 |
+
if text:
|
83 |
+
# Start with 1-based indexing for readability
|
84 |
+
header = f"Source {index + 1} (ID: {doc_id}"
|
85 |
+
if source_name:
|
86 |
+
header += f", SourceName: {source_name}"
|
87 |
+
header += ")"
|
88 |
+
formatted_docs.append(f"{header}:\n{text}\n---") # Add separator
|
89 |
+
|
90 |
+
if not formatted_docs:
|
91 |
+
return "No valid source texts could be formatted."
|
92 |
+
|
93 |
+
return "\n".join(formatted_docs)
|