ABE101 commited on
Commit
1835c79
·
verified ·
1 Parent(s): 0e1af46

Upload 11 files

Browse files
Files changed (11) hide show
  1. .gitignore +160 -0
  2. .replit +55 -0
  3. app.py +232 -0
  4. config.py +79 -0
  5. generated-icon.png +0 -0
  6. main.py +0 -0
  7. pyproject.toml +7 -0
  8. rag_processor.py +152 -0
  9. requirements.txt +8 -0
  10. utils.py +93 -0
  11. uv.lock +7 -0
.gitignore ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
.replit ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .replit
2
+ # Specifies the command to run the app
3
+ run = "streamlit run app.py"
4
+
5
+ # The main entry point file
6
+ entrypoint = "app.py"
7
+ modules = ["python-3.11"]
8
+
9
+ # Tells Replit's package manager about dependencies
10
+ [packager]
11
+ language = "python3"
12
+
13
+ # Optional: Ignore specific packages if needed, though usually handled by requirements.txt
14
+ # ignoredPackages = []
15
+
16
+ # Optional: Improves Python development experience in Replit
17
+ [languages.python]
18
+ pattern = "**/*.py"
19
+ [languages.python.languageServer]
20
+ name = "pyright"
21
+
22
+ [deployment]
23
+ deploymentTarget = "cloudrun"
24
+ run = ["sh", "-c", "streamlit run --server.address 0.0.0.0 --server.headless true --server.enableCORS=false --server.enableWebsocketCompression=false app.py"]
25
+ build = ["sh", "-c", "pip install --upgrade pip"]
26
+
27
+ [nix]
28
+
29
+ [[ports]]
30
+ localPort = 8501
31
+ externalPort = 80
32
+
33
+ [[ports]]
34
+ localPort = 8502
35
+ externalPort = 3000
36
+
37
+ [[ports]]
38
+ localPort = 8503
39
+ externalPort = 3001
40
+
41
+ [workflows]
42
+ runButton = "Run"
43
+
44
+ [[workflows.workflow]]
45
+ name = "Run"
46
+ author = 22737092
47
+ mode = "sequential"
48
+
49
+ [[workflows.workflow.tasks]]
50
+ task = "shell.exec"
51
+ args = "streamlit run app.py"
52
+
53
+ # Environment variables can also be set here, but Secrets are recommended for API keys
54
+ # [env]
55
+ # LANGSMITH_TRACING = "true"
app.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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("<hr>", unsafe_allow_html=True)
62
+ st.sidebar.markdown(
63
+ f"<p class='rtl-text'><strong>OpenAI ({config.OPENAI_VALIDATION_MODEL} / {config.OPENAI_GENERATION_MODEL}):</strong> {'✅' if openai_ready else '❌'}</p>",
64
+ unsafe_allow_html=True
65
+ )
66
+ if not openai_ready:
67
+ st.sidebar.error("OpenAI אינו זמין.", icon="⚠️")
68
+ st.sidebar.markdown("<hr>", unsafe_allow_html=True)
69
+ st.sidebar.markdown("<h3 class='rtl-text'>הגדרות חיפוש</h3>", unsafe_allow_html=True)
70
+ n_retrieve = st.sidebar.slider("מספר פסקאות לאחזור", 1, 300, config.DEFAULT_N_RETRIEVE)
71
+ max_validate = min(n_retrieve, 100)
72
+ n_validate = st.sidebar.slider(
73
+ "פסקאות לאימות (GPT-4o)",
74
+ 1,
75
+ max_validate,
76
+ min(config.DEFAULT_N_VALIDATE, max_validate),
77
+ disabled=not openai_ready
78
+ )
79
+ st.sidebar.info("התשובות מבוססות רק על המקורות שאומתו.", icon="ℹ️")
80
+ return {"n_retrieve": n_retrieve, "n_validate": n_validate, "services_ready": (retriever_ready and openai_ready)}
81
+
82
+
83
+ def display_chat_message(message: Dict[str, Any]):
84
+ role = message.get("role", "assistant")
85
+ with st.chat_message(role):
86
+ st.markdown(message.get('content', ''), unsafe_allow_html=True)
87
+ if role == "assistant" and message.get("final_docs"):
88
+ docs = message["final_docs"]
89
+ exp_title = f"<span class='rtl-text'>הצג {len(docs)} קטעי מקור שנשלחו למחולל (GPT-4o)</span>"
90
+ with st.expander(exp_title, expanded=False):
91
+ st.markdown("<div dir='rtl' class='expander-content'>", unsafe_allow_html=True)
92
+ for i, doc in enumerate(docs, start=1):
93
+ if not isinstance(doc, dict):
94
+ continue
95
+ source = doc.get('source_name', '') or 'מקור לא ידוע'
96
+ text = clean_source_text(doc.get('hebrew_text', ''))
97
+ st.markdown(
98
+ f"<div class='source-info rtl-text'><strong>מקור {i}:</strong> {source}</div>",
99
+ unsafe_allow_html=True
100
+ )
101
+ st.markdown(f"<div class='hebrew-text'>{text}</div>", unsafe_allow_html=True)
102
+ st.markdown("</div>", unsafe_allow_html=True)
103
+
104
+
105
+ def display_status_updates(status_log: List[str]):
106
+ if status_log:
107
+ with st.expander("<span class='rtl-text'>הצג פרטי עיבוד</span>", expanded=False):
108
+ for u in status_log:
109
+ st.markdown(
110
+ f"<code class='status-update rtl-text'>- {u}</code>",
111
+ unsafe_allow_html=True
112
+ )
113
+
114
+ # --- Main Application Logic ---
115
+ if "messages" not in st.session_state:
116
+ st.session_state.messages = []
117
+
118
+ rag_params = display_sidebar()
119
+
120
+ # Render history
121
+ for msg in st.session_state.messages:
122
+ display_chat_message(msg)
123
+
124
+ if prompt := st.chat_input("שאל שאלה בענייני חסידות...", disabled=not rag_params["services_ready"]):
125
+ st.session_state.messages.append({"role": "user", "content": prompt})
126
+ display_chat_message(st.session_state.messages[-1])
127
+
128
+ with st.chat_message("assistant"):
129
+ msg_placeholder = st.empty()
130
+ status_container = st.status("מעבד בקשה...", expanded=True)
131
+ chunks: List[str] = []
132
+ try:
133
+ def status_cb(m):
134
+ status_container.update(label=f"<span class='rtl-text'>{m}</span>")
135
+ def stream_cb(c):
136
+ chunks.append(c)
137
+ msg_placeholder.markdown(
138
+ f"<div dir='rtl' class='rtl-text'>{''.join(chunks)}▌</div>",
139
+ unsafe_allow_html=True
140
+ )
141
+
142
+ loop = asyncio.get_event_loop()
143
+ final_rag = loop.run_until_complete(
144
+ execute_validate_generate_pipeline(
145
+ history=st.session_state.messages,
146
+ params=rag_params,
147
+ status_callback=status_cb,
148
+ stream_callback=stream_cb
149
+ )
150
+ )
151
+
152
+ if isinstance(final_rag, dict):
153
+ raw = final_rag.get("final_response", "")
154
+ err = final_rag.get("error")
155
+ log = final_rag.get("status_log", [])
156
+ docs = final_rag.get("generator_input_documents", [])
157
+ pipeline = final_rag.get("pipeline_used", PIPELINE_VALIDATE_GENERATE_GPT4O)
158
+
159
+ # wrap in RTL div if needed
160
+ final = raw
161
+ if not (err and final.strip().startswith("<div")) and not final.strip().startswith((
162
+ '<div', '<p', '<ul', '<ol', '<strong'
163
+ )):
164
+ final = f"<div dir='rtl' class='rtl-text'>{final or 'לא התקבלה תשובה מהמחולל.'}</div>"
165
+ msg_placeholder.markdown(final, unsafe_allow_html=True)
166
+
167
+ # --- Show only cited paragraphs ---
168
+ cited_ids = set(re.findall(r'\(מקור\s*([0-9]+)\)', raw))
169
+ if cited_ids:
170
+ enumerated_docs = list(enumerate(docs, start=1))
171
+ docs_to_show = [(idx, doc) for idx, doc in enumerated_docs if str(idx) in cited_ids]
172
+ else:
173
+ docs_to_show = list(enumerate(docs, start=1))
174
+
175
+ if docs_to_show:
176
+ label = f"<span class='rtl-text'>הצג {len(docs_to_show)} קטעי מקור שהוזכרו בתשובה</span>"
177
+ with st.expander(label, expanded=False):
178
+ st.markdown("<div dir='rtl' class='expander-content'>", unsafe_allow_html=True)
179
+ for idx, doc in docs_to_show:
180
+ source = doc.get('source_name', '') or 'מקור לא ידוע'
181
+ text = clean_source_text(doc.get('hebrew_text', ''))
182
+ st.markdown(
183
+ f"<div class='source-info rtl-text'><strong>מקור {idx}:</strong> {source}</div>",
184
+ unsafe_allow_html=True
185
+ )
186
+ st.markdown(f"<div class='hebrew-text'>{text}</div>", unsafe_allow_html=True)
187
+ st.markdown("</div>", unsafe_allow_html=True)
188
+ # --- end filter display ---
189
+
190
+ # store assistant message
191
+ assistant_data = {
192
+ "role": "assistant",
193
+ "content": final,
194
+ "final_docs": docs,
195
+ "pipeline_used": pipeline,
196
+ "status_log": log,
197
+ "error": err
198
+ }
199
+ st.session_state.messages.append(assistant_data)
200
+ display_status_updates(log)
201
+ if err:
202
+ status_container.update(label="שגיאה בעיבוד!", state="error", expanded=False)
203
+ else:
204
+ status_container.update(label="העיבוד הושלם!", state="complete", expanded=False)
205
+ else:
206
+ msg_placeholder.markdown(
207
+ "<div dir='rtl' class='rtl-text'><strong>שגיאה בלתי צפויה בתקשורת.</strong></div>",
208
+ unsafe_allow_html=True
209
+ )
210
+ st.session_state.messages.append({
211
+ "role": "assistant",
212
+ "content": "שגיאה בלתי צפויה בתקשורת.",
213
+ "final_docs": [],
214
+ "pipeline_used": "Error",
215
+ "status_log": ["Unexpected result"],
216
+ "error": "Unexpected"
217
+ })
218
+ status_container.update(label="שגיאה בלתי צפויה!", state="error", expanded=False)
219
+ except Exception as e:
220
+ traceback.print_exc()
221
+ err_html = (f"<div dir='rtl' class='rtl-text'><strong>שגיאה קריטית!</strong><br>נסה לרענן."
222
+ f"<details><summary>פרטים</summary><pre>{traceback.format_exc()}</pre></details></div>")
223
+ msg_placeholder.error(err_html, icon="🔥")
224
+ st.session_state.messages.append({
225
+ "role": "assistant",
226
+ "content": err_html,
227
+ "final_docs": [],
228
+ "pipeline_used": "Critical Error",
229
+ "status_log": [f"Critical: {type(e).__name__}"],
230
+ "error": str(e)
231
+ })
232
+ status_container.update(label=str(e), state="error", expanded=False)
config.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py (Updated for OpenAI Generation)
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ # --- LangSmith Configuration ---
8
+ LANGSMITH_ENDPOINT = os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
9
+ LANGSMITH_TRACING = os.environ.get("LANGSMITH_TRACING", "true")
10
+ LANGSMITH_API_KEY = os.environ.get("LANGSMITH_API_KEY")
11
+ LANGSMITH_PROJECT = os.environ.get("LANGSMITH_PROJECT", "DivreyYoel-RAG-GPT4-Gen") # Updated project name
12
+
13
+ # --- API Keys (Required) ---
14
+ # ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") # No longer needed
15
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") # Used for Embedding, Validation, AND Generation
16
+ PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
17
+
18
+ # --- Model Configuration ---
19
+ # Embedding Model (Retriever)
20
+ EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-large")
21
+ # Validation Model (OpenAI)
22
+ OPENAI_VALIDATION_MODEL = os.environ.get("OPENAI_VALIDATION_MODEL", "gpt-4o")
23
+ # Generation Model (OpenAI) - Can be the same or different from validation
24
+ OPENAI_GENERATION_MODEL = os.environ.get("OPENAI_GENERATION_MODEL", "gpt-4o") # Using GPT-4o for generation too
25
+
26
+ # --- Pinecone Configuration ---
27
+ PINECONE_INDEX_NAME = os.environ.get("PINECONE_INDEX_NAME", "chassidus-index")
28
+
29
+ # --- Default RAG Pipeline Parameters ---
30
+ DEFAULT_N_RETRIEVE = 100
31
+ DEFAULT_N_VALIDATE = 50
32
+
33
+ # --- System Prompts ---
34
+ # --- REMOVED ANTHROPIC_SYSTEM_PROMPT ---
35
+
36
+ # --- NEW OpenAI System Prompt ---
37
+ OPENAI_SYSTEM_PROMPT = """You are an expert assistant specializing in Chassidic texts, particularly the works of the Satmar Rebbe, Rabbi Yoel Teitelbaum (Divrei Yoel).
38
+ 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.
39
+
40
+ **Source Text Format:**
41
+ The relevant source texts will be provided below under the heading "Source Texts:". Each source is numbered and includes an ID.
42
+
43
+ **Response Requirements:**
44
+ 1. **Language:** Respond **exclusively in Hebrew**.
45
+ 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.
46
+ 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.
47
+ 4. **Completeness:** Synthesize information from *multiple* relevant sources if they contribute to the answer.
48
+ 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.
49
+ 6. **Clarity and Conciseness:** Provide a clear, well-structured, and concise answer in Hebrew. Focus on directly answering the user's question.
50
+ 7. **Tone:** Maintain a formal and respectful tone appropriate for discussing religious texts.
51
+ 8. **No Greetings/Closings:** Do not include introductory greetings (e.g., "שלום") or concluding remarks (e.g., "בברכה", "מקווה שעזרתי"). Focus solely on the answer.
52
+ """
53
+
54
+ # --- Helper Functions ---
55
+ def check_env_vars():
56
+ """Checks if essential API keys are present."""
57
+ missing_keys = []
58
+ if not LANGSMITH_API_KEY: missing_keys.append("LANGSMITH_API_KEY")
59
+ # if not ANTHROPIC_API_KEY: missing_keys.append("ANTHROPIC_API_KEY") # Removed check
60
+ if not OPENAI_API_KEY: missing_keys.append("OPENAI_API_KEY")
61
+ if not PINECONE_API_KEY: missing_keys.append("PINECONE_API_KEY")
62
+ return missing_keys
63
+
64
+ def configure_langsmith():
65
+ """Sets LangSmith environment variables."""
66
+ os.environ["LANGSMITH_ENDPOINT"] = LANGSMITH_ENDPOINT
67
+ os.environ["LANGSMITH_TRACING"] = LANGSMITH_TRACING
68
+ if LANGSMITH_API_KEY: os.environ["LANGSMITH_API_KEY"] = LANGSMITH_API_KEY
69
+ if LANGSMITH_PROJECT: os.environ["LANGSMITH_PROJECT"] = LANGSMITH_PROJECT
70
+ print(f"LangSmith configured: Endpoint={LANGSMITH_ENDPOINT}, Tracing={LANGSMITH_TRACING}, Project={LANGSMITH_PROJECT or 'Default'}")
71
+
72
+ # --- Initial Check on Import ---
73
+ missing = check_env_vars()
74
+ if missing:
75
+ print(f"Warning: Missing essential API keys in Replit Secrets: {', '.join(missing)}")
76
+ else:
77
+ print("All essential API keys found in environment/secrets.")
78
+
79
+ configure_langsmith()
generated-icon.png ADDED
main.py ADDED
File without changes
pyproject.toml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "python-template"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["Your Name <you@example.com>"]
6
+ requires-python = ">=3.11"
7
+ dependencies = []
rag_processor.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag_processor.py (Fixed Syntax Error AGAIN)
2
+ import time
3
+ import asyncio
4
+ import traceback
5
+ from typing import List, Dict, Any, Optional, Callable, Tuple
6
+ from langsmith import traceable
7
+
8
+ try:
9
+ import config
10
+ from services import retriever, openai_service
11
+ except ImportError:
12
+ print("Error: Failed to import config or services in rag_processor.py")
13
+ raise SystemExit("Failed imports in rag_processor.py")
14
+
15
+ PIPELINE_VALIDATE_GENERATE_GPT4O = "GPT-4o Validator + GPT-4o Synthesizer"
16
+ StatusCallback = Callable[[str], None]
17
+
18
+ # --- Step Functions ---
19
+
20
+ @traceable(name="rag-step-retrieve")
21
+ async def run_retrieval_step(query: str, n_retrieve: int, update_status: StatusCallback) -> List[Dict]:
22
+ update_status(f"1. מאחזר עד {n_retrieve} פסקאות מ-Pinecone...")
23
+ start_time = time.time(); retrieved_docs = retriever.retrieve_documents(query_text=query, n_results=n_retrieve)
24
+ retrieval_time = time.time() - start_time; status_msg = f"אוחזרו {len(retrieved_docs)} פסקאות ב-{retrieval_time:.2f} שניות."
25
+ update_status(f"1. {status_msg}")
26
+ if not retrieved_docs: update_status("1. לא אותרו מסמכים.")
27
+ return retrieved_docs
28
+
29
+ @traceable(name="rag-step-gpt4o-filter")
30
+ async def run_gpt4o_validation_filter_step(
31
+ docs_to_process: List[Dict], query: str, n_validate: int, update_status: StatusCallback
32
+ ) -> List[Dict]:
33
+ if not docs_to_process: update_status("2. [GPT-4o] דילוג על אימות - אין פסקאות."); return []
34
+ validation_count = min(len(docs_to_process), n_validate)
35
+ update_status(f"2. [GPT-4o] מתחיל אימות מקבילי ({validation_count} / {len(docs_to_process)} פסקאות)...")
36
+ validation_start_time = time.time()
37
+ tasks = [openai_service.validate_relevance_openai(doc, query, i) for i, doc in enumerate(docs_to_process[:validation_count])]
38
+ validation_results = await asyncio.gather(*tasks, return_exceptions=True)
39
+ passed_docs = []; passed_count, failed_validation_count, error_count = 0, 0, 0
40
+ update_status("3. [GPT-4o] סינון פסקאות לפי תוצאות אימות...")
41
+ for i, res in enumerate(validation_results):
42
+ original_doc = docs_to_process[i]
43
+ if isinstance(res, Exception): print(f"GPT-4o Validation Exception doc {i}: {res}"); error_count += 1
44
+ elif isinstance(res, dict) and 'validation' in res and 'paragraph_data' in res:
45
+ if res["validation"].get("contains_relevant_info") is True:
46
+ original_doc['validation_result'] = res["validation"]; passed_docs.append(original_doc); passed_count += 1
47
+ else: failed_validation_count += 1
48
+ else: print(f"GPT-4o Validation Unexpected result doc {i}: {type(res)}"); error_count += 1
49
+ validation_time = time.time() - validation_start_time
50
+ status_msg_val = f"אימות GPT-4o הושלם ({passed_count} עברו, {failed_validation_count} נדחו, {error_count} שגיאות) ב-{validation_time:.2f} שניות."
51
+ update_status(f"2. {status_msg_val}")
52
+ status_msg_filter = f"נאספו {len(passed_docs)} פסקאות רלוונטיות לאחר אימות GPT-4o."
53
+ update_status(f"3. {status_msg_filter}")
54
+ return passed_docs # Returns full docs that passed
55
+
56
+ @traceable(name="rag-step-openai-generate")
57
+ async def run_openai_generation_step(
58
+ history: List[Dict], context_documents: List[Dict], update_status: StatusCallback, stream_callback: Callable[[str], None]
59
+ ) -> Tuple[str, Optional[str]]:
60
+ generator_name = "OpenAI";
61
+ if not context_documents:
62
+ update_status(f"4. [{generator_name}] דילוג על יצירה - אין פסקאות להקשר.")
63
+ return "לא סופקו פסקאות רלוונטיות ליצירת התשובה.", None
64
+ update_status(f"4. [{generator_name}] מחולל תשובה סופית מ-{len(context_documents)} קטעי הקשר...")
65
+ start_gen_time = time.time()
66
+ try:
67
+ full_response = []; error_msg = None
68
+ generator = openai_service.generate_openai_stream(messages=history, context_documents=context_documents)
69
+ async for chunk in generator:
70
+ if isinstance(chunk, str) and chunk.strip().startswith("--- Error:"):
71
+ if not error_msg: error_msg = chunk.strip()
72
+ print(f"OpenAI stream yielded error: {chunk.strip()}"); break
73
+ elif isinstance(chunk, str): full_response.append(chunk); stream_callback(chunk)
74
+ final_response_text = "".join(full_response); gen_time = time.time() - start_gen_time
75
+ if error_msg: update_status(f"4. שגיאה ביצירת התשובה ({generator_name}) ב-{gen_time:.2f} שניות."); return final_response_text, error_msg
76
+ else: update_status(f"4. יצירת התשובה ({generator_name}) הושלמה ב-{gen_time:.2f} שניות."); return final_response_text, None
77
+ except Exception as gen_err:
78
+ gen_time = time.time() - start_gen_time; error_msg_critical = f"--- Error: Critical failure during {generator_name} generation ({type(gen_err).__name__}): {gen_err} ---"
79
+ update_status(f"4. שגיאה קריטית ביצירת התשובה ({generator_name}) ב-{gen_time:.2f} שניות."); traceback.print_exc(); return "", error_msg_critical
80
+
81
+
82
+ # --- Main Pipeline Orchestrator ---
83
+ @traceable(name="rag-execute-validate-generate-gpt4o-pipeline")
84
+ async def execute_validate_generate_pipeline(
85
+ history: List[Dict], params: Dict[str, Any], status_callback: StatusCallback, stream_callback: Callable[[str], None]
86
+ ) -> Dict[str, Any]:
87
+ """
88
+ Orchestrates Retrieve -> Validate (GPT-4o) -> Generate (GPT-4o) pipeline.
89
+ Stores both full validated docs and simplified docs for generator input.
90
+ """
91
+ result = { "final_response": "", "validated_documents_full": [], "generator_input_documents": [], "status_log": [], "error": None, "pipeline_used": PIPELINE_VALIDATE_GENERATE_GPT4O }
92
+
93
+ # --- Corrected Initialization ---
94
+ status_log_internal = [] # Initialize the list on its own line
95
+
96
+ # Define the helper function on the next line, correctly indented
97
+ def update_status_and_log(message: str):
98
+ print(f"Status Update: {message}") # Log status to console
99
+ status_log_internal.append(message)
100
+ status_callback(message) # Update UI
101
+ # ------------------------------
102
+
103
+ current_query_text = ""
104
+ if history and isinstance(history, list):
105
+ for msg_ in reversed(history):
106
+ if isinstance(msg_, dict) and msg_.get("role") == "user": current_query_text = str(msg_.get("content") or ""); break
107
+ if not current_query_text: print("Error: Could not extract query."); result["error"] = "לא זוהתה שאלה."; result["final_response"] = f"<div class='rtl-text'>{result['error']}</div>"; result["status_log"] = status_log_internal; return result
108
+
109
+ try:
110
+ # --- 1. Retrieval ---
111
+ retrieved_docs = await run_retrieval_step(current_query_text, params['n_retrieve'], update_status_and_log)
112
+ if not retrieved_docs: result["error"] = "לא אותרו מקורות."; result["final_response"] = f"<div class='rtl-text'>{result['error']}</div>"; result["status_log"] = status_log_internal; return result
113
+
114
+ # --- 2. Validation ---
115
+ validated_docs_full = await run_gpt4o_validation_filter_step(retrieved_docs, current_query_text, params['n_validate'], update_status_and_log)
116
+ result["validated_documents_full"] = validated_docs_full # Store full docs for trace/debug
117
+ if not validated_docs_full: result["error"] = "לא נמצאו פסקאות רלוונטיות."; result["final_response"] = f"<div class='rtl-text'>{result['error']}</div>"; result["status_log"] = status_log_internal; update_status_and_log(f"4. {result['error']} לא ניתן להמשיך."); return result
118
+
119
+ # --- Simplify Docs for Generation ---
120
+ simplified_docs_for_generation = []
121
+ print(f"Processor: Simplifying {len(validated_docs_full)} docs...");
122
+ for doc in validated_docs_full:
123
+ if isinstance(doc, dict):
124
+ hebrew_text = doc.get('hebrew_text', '')
125
+ if hebrew_text:
126
+ simplified_doc = {'hebrew_text': hebrew_text, 'original_id': doc.get('original_id', 'unknown'), 'source_name': doc.get('source_name', '')}
127
+ if not simplified_doc['source_name']: del simplified_doc['source_name']
128
+ simplified_docs_for_generation.append(simplified_doc)
129
+ else: print(f"Warn: Skipping non-dict item: {doc}")
130
+ result["generator_input_documents"] = simplified_docs_for_generation # Store simplified for UI
131
+ print(f"Processor: Created {len(simplified_docs_for_generation)} simplified docs.")
132
+
133
+ # --- 3. Generation ---
134
+ final_response_text, generation_error = await run_openai_generation_step(
135
+ history=history, context_documents=simplified_docs_for_generation, # Pass simplified list
136
+ update_status=update_status_and_log, stream_callback=stream_callback
137
+ )
138
+ result["final_response"] = final_response_text; result["error"] = generation_error
139
+
140
+ # Handle display of potential errors
141
+ if generation_error and not result["final_response"].strip().startswith(("<div", "לא סופקו")):
142
+ result["final_response"] = f"<div class='rtl-text'><strong>שגיאה ביצירת התשובה.</strong><br>פרטים: {generation_error}<br>---<br>{result['final_response']}</div>"
143
+ elif result["final_response"] == "לא סופקו פסקאות רלוונטיות ליצירת התשובה.":
144
+ result["final_response"] = f"<div class='rtl-text'>{result['final_response']}</div>"
145
+
146
+ except Exception as e: # General catch-all
147
+ error_type = type(e).__name__; error_msg = f"שגיאה קריטית RAG ({error_type}): {e}"; print(f"Critical RAG Error: {error_msg}"); traceback.print_exc()
148
+ result["error"] = error_msg; result["final_response"] = f"<div class='rtl-text'><strong>שגיאה קריטית! ({error_type})</strong><br>נסה שוב.<details><summary>פרטים</summary><pre>{traceback.format_exc()}</pre></details></div>"
149
+ update_status_and_log(f"שגיאה קריטית: {error_type}")
150
+
151
+ result["status_log"] = status_log_internal
152
+ return result
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+
2
+ streamlit
3
+ python-dotenv
4
+ pinecone
5
+ openai
6
+ anthropic
7
+ langsmith
8
+ nest_asyncio
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)
uv.lock ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ requires-python = ">=3.11"
3
+
4
+ [[package]]
5
+ name = "python-template"
6
+ version = "0.1.0"
7
+ source = { virtual = "." }