ABE101 commited on
Commit
ae4184d
·
verified ·
1 Parent(s): 4ac14be

Upload 5 files

Browse files
Files changed (5) hide show
  1. app.py +247 -0
  2. code (9).txt +7 -0
  3. config.py +87 -0
  4. rag_processor.py +219 -0
  5. 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)