Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- .gitignore +160 -0
- .replit +55 -0
- app.py +232 -0
- config.py +79 -0
- generated-icon.png +0 -0
- main.py +0 -0
- pyproject.toml +7 -0
- rag_processor.py +152 -0
- requirements.txt +8 -0
- utils.py +93 -0
- 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 = "." }
|