Spaces:
Running
Running
Upload 3 files
Browse files- _initi_.py +2 -0
- openai_service.py +158 -0
- retriever.py +88 -0
_initi_.py
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
# services/__init__.py
|
2 |
+
# This file can be empty
|
openai_service.py
ADDED
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# services/openai_service.py (Added Generation Function)
|
2 |
+
import openai
|
3 |
+
import traceback
|
4 |
+
import json
|
5 |
+
import asyncio
|
6 |
+
from typing import Dict, Optional, Tuple, List, AsyncGenerator # Added List, AsyncGenerator
|
7 |
+
from langsmith import traceable
|
8 |
+
|
9 |
+
try:
|
10 |
+
import config
|
11 |
+
from utils import format_context_for_openai # Import new formatter
|
12 |
+
except ImportError:
|
13 |
+
print("Error: Failed to import config or utils in openai_service.py")
|
14 |
+
raise SystemExit("Failed imports in openai_service.py")
|
15 |
+
|
16 |
+
# --- Globals ---
|
17 |
+
openai_async_client: Optional[openai.AsyncOpenAI] = None
|
18 |
+
is_openai_ready: bool = False
|
19 |
+
openai_status_message: str = "OpenAI service not initialized."
|
20 |
+
|
21 |
+
# --- Initialization ---
|
22 |
+
def init_openai_client() -> Tuple[bool, str]:
|
23 |
+
"""Initializes the OpenAI async client."""
|
24 |
+
global openai_async_client, is_openai_ready, openai_status_message
|
25 |
+
if is_openai_ready: return True, openai_status_message
|
26 |
+
if not config.OPENAI_API_KEY:
|
27 |
+
openai_status_message = "Error: OPENAI_API_KEY not found in Secrets."
|
28 |
+
is_openai_ready = False; return False, openai_status_message
|
29 |
+
try:
|
30 |
+
openai_async_client = openai.AsyncOpenAI(api_key=config.OPENAI_API_KEY)
|
31 |
+
# Update status message to reflect dual use
|
32 |
+
openai_status_message = f"OpenAI service ready (Validate: {config.OPENAI_VALIDATION_MODEL}, Generate: {config.OPENAI_GENERATION_MODEL})."
|
33 |
+
is_openai_ready = True
|
34 |
+
print("OpenAI Service: Async client initialized.")
|
35 |
+
return True, openai_status_message
|
36 |
+
except Exception as e:
|
37 |
+
error_msg = f"Error initializing OpenAI async client: {type(e).__name__} - {e}"; print(error_msg); traceback.print_exc()
|
38 |
+
openai_status_message = error_msg; is_openai_ready = False; openai_async_client = None
|
39 |
+
return False, openai_status_message
|
40 |
+
|
41 |
+
def get_openai_status() -> Tuple[bool, str]:
|
42 |
+
"""Returns the current status of the OpenAI service."""
|
43 |
+
if not is_openai_ready: init_openai_client()
|
44 |
+
return is_openai_ready, openai_status_message
|
45 |
+
|
46 |
+
# --- Validation Function (Keep As Is) ---
|
47 |
+
@traceable(name="openai-validate-paragraph")
|
48 |
+
async def validate_relevance_openai(
|
49 |
+
paragraph_data: Dict, user_question: str, paragraph_index: int
|
50 |
+
) -> Optional[Dict]:
|
51 |
+
# ... (Keep the existing implementation of validate_relevance_openai) ...
|
52 |
+
global openai_async_client; ready, msg = get_openai_status()
|
53 |
+
if not ready or openai_async_client is None: print(f"OpenAI validation failed (Para {paragraph_index+1}): Client not ready - {msg}"); return None
|
54 |
+
safe_paragraph_data = paragraph_data.copy() if isinstance(paragraph_data, dict) else {}
|
55 |
+
if not paragraph_data or not isinstance(paragraph_data, dict):
|
56 |
+
return {"validation": {"contains_relevant_info": False, "justification": "Input data empty/invalid."}, "paragraph_data": safe_paragraph_data}
|
57 |
+
hebrew_text = paragraph_data.get('hebrew_text', '').strip(); english_text = paragraph_data.get('english_text', '').strip()
|
58 |
+
if not hebrew_text and not english_text: return {"validation": {"contains_relevant_info": False, "justification": "Paragraph text empty."}, "paragraph_data": safe_paragraph_data}
|
59 |
+
validation_model = config.OPENAI_VALIDATION_MODEL
|
60 |
+
prompt_content = f"""User Question (Hebrew):\n"{user_question}"\n\nText Paragraph (Paragraph {paragraph_index+1}):\nHebrew:\n---\n{hebrew_text or "(No Hebrew)"}\n---\nEnglish:\n---\n{english_text or "(No English)"}\n---\n\nInstruction:\nAnalyze the Text Paragraph. Determine if it contains information that *directly* answers or significantly contributes to answering the User Question.\nRespond ONLY with valid JSON: {{"contains_relevant_info": boolean, "justification": "Brief Hebrew explanation"}}\nExample: {{"contains_relevant_info": true, "justification": "..."}} OR {{"contains_relevant_info": false, "justification": "..."}}\nOutput only the JSON object."""
|
61 |
+
try:
|
62 |
+
response = await openai_async_client.chat.completions.create(model=validation_model, messages=[{"role": "user", "content": prompt_content}], temperature=0.1, max_tokens=150, response_format={"type": "json_object"})
|
63 |
+
json_string = response.choices[0].message.content
|
64 |
+
try:
|
65 |
+
validation_result = json.loads(json_string)
|
66 |
+
if not isinstance(validation_result, dict) or 'contains_relevant_info' not in validation_result or 'justification' not in validation_result or not isinstance(validation_result['contains_relevant_info'], bool) or not isinstance(validation_result['justification'], str):
|
67 |
+
print(f"Error (OpenAI Validate {paragraph_index+1}): Invalid JSON structure: {validation_result}")
|
68 |
+
return {"validation": {"contains_relevant_info": False, "justification": "Error: Invalid response format."}, "paragraph_data": safe_paragraph_data}
|
69 |
+
return {"validation": validation_result, "paragraph_data": safe_paragraph_data}
|
70 |
+
except json.JSONDecodeError as json_err:
|
71 |
+
print(f"Error (OpenAI Validate {paragraph_index+1}): Failed JSON decode: {json_err}. Response: {json_string}")
|
72 |
+
return {"validation": {"contains_relevant_info": False, "justification": "Error: Failed to parse JSON response."}, "paragraph_data": safe_paragraph_data}
|
73 |
+
except openai.RateLimitError as e: print(f"Error (OpenAI Validate {paragraph_index+1}): Rate Limit: {e}"); return {"validation": {"contains_relevant_info": False, "justification": "Error: Rate limit hit."}, "paragraph_data": safe_paragraph_data}
|
74 |
+
except openai.APIConnectionError as e: print(f"Error (OpenAI Validate {paragraph_index+1}): Connection Error: {e}"); return None
|
75 |
+
except openai.APIStatusError as e: print(f"Error (OpenAI Validate {paragraph_index+1}): API Status {e.status_code}: {e.response}"); return None
|
76 |
+
except Exception as e: print(f"Error (OpenAI Validate {paragraph_index+1}): Unexpected: {type(e).__name__}"); traceback.print_exc(); return None
|
77 |
+
|
78 |
+
|
79 |
+
# --- NEW Generation Function ---
|
80 |
+
@traceable(name="openai-generate-stream")
|
81 |
+
async def generate_openai_stream(
|
82 |
+
messages: List[Dict],
|
83 |
+
context_documents: List[Dict],
|
84 |
+
) -> AsyncGenerator[str, None]:
|
85 |
+
"""
|
86 |
+
Generates a response stream using OpenAI GPT model based on history and context.
|
87 |
+
Yields text chunks or an error message string.
|
88 |
+
"""
|
89 |
+
global openai_async_client
|
90 |
+
ready, msg = get_openai_status()
|
91 |
+
if not ready or openai_async_client is None:
|
92 |
+
yield f"--- Error: OpenAI client not available for generation: {msg} ---"
|
93 |
+
return
|
94 |
+
|
95 |
+
try:
|
96 |
+
# Validate context format
|
97 |
+
if not isinstance(context_documents, list) or not all(isinstance(item, dict) for item in context_documents):
|
98 |
+
yield f"--- Error: Invalid format for context_documents (expected List[Dict]). ---"
|
99 |
+
return
|
100 |
+
|
101 |
+
# Format context using the new utility function
|
102 |
+
formatted_context = format_context_for_openai(context_documents)
|
103 |
+
if not formatted_context or formatted_context.startswith("No"): # Check for empty or failed formatting
|
104 |
+
yield f"--- Error: No valid context provided or formatted for OpenAI generator. ---"
|
105 |
+
return
|
106 |
+
|
107 |
+
# Find the latest user message from history
|
108 |
+
last_user_msg_content = "User question not found."
|
109 |
+
if messages and isinstance(messages, list):
|
110 |
+
for msg_ in reversed(messages):
|
111 |
+
if isinstance(msg_, dict) and msg_.get("role") == "user":
|
112 |
+
last_user_msg_content = str(msg_.get("content") or "")
|
113 |
+
break
|
114 |
+
|
115 |
+
# Construct the final user prompt for the generation model
|
116 |
+
user_prompt_content = f"Source Texts:\n{formatted_context}\n\nUser Question:\n{last_user_msg_content}\n\nAnswer (in Hebrew, based ONLY on the Source Texts provided):"
|
117 |
+
|
118 |
+
# Prepare messages for the API call - System Prompt + User Prompt
|
119 |
+
api_messages = [
|
120 |
+
{"role": "system", "content": config.OPENAI_SYSTEM_PROMPT},
|
121 |
+
{"role": "user", "content": user_prompt_content}
|
122 |
+
]
|
123 |
+
|
124 |
+
generation_model = config.OPENAI_GENERATION_MODEL
|
125 |
+
print(f" -> Sending stream request to OpenAI (Model: {generation_model})...")
|
126 |
+
print(f" -> User Prompt Content (start): {user_prompt_content[:300]}...") # Log start of prompt
|
127 |
+
|
128 |
+
# Make the streaming API call
|
129 |
+
stream = await openai_async_client.chat.completions.create(
|
130 |
+
model=generation_model,
|
131 |
+
messages=api_messages,
|
132 |
+
temperature=0.5, # Adjust temperature as needed
|
133 |
+
max_tokens=3000, # Set a reasonable max token limit
|
134 |
+
stream=True
|
135 |
+
)
|
136 |
+
|
137 |
+
print(f" -> OpenAI stream processing...")
|
138 |
+
async for chunk in stream:
|
139 |
+
content = chunk.choices[0].delta.content
|
140 |
+
if content is not None:
|
141 |
+
yield content # Yield the text chunk
|
142 |
+
# Add a small sleep to avoid blocking the event loop entirely
|
143 |
+
await asyncio.sleep(0.01)
|
144 |
+
print(f" -> OpenAI stream finished.")
|
145 |
+
|
146 |
+
# --- Exception Handling ---
|
147 |
+
except openai.RateLimitError as e:
|
148 |
+
error_msg = f"\n\n--- Error: OpenAI rate limit exceeded during generation: {e} ---"
|
149 |
+
print(error_msg); traceback.print_exc(); yield error_msg
|
150 |
+
except openai.APIConnectionError as e:
|
151 |
+
error_msg = f"\n\n--- Error: OpenAI connection error during generation: {e} ---"
|
152 |
+
print(error_msg); traceback.print_exc(); yield error_msg
|
153 |
+
except openai.APIStatusError as e:
|
154 |
+
error_msg = f"\n\n--- Error: OpenAI API status error ({e.status_code}) during generation: {e.response} ---"
|
155 |
+
print(error_msg); traceback.print_exc(); yield error_msg
|
156 |
+
except Exception as e:
|
157 |
+
error_msg = f"\n\n--- Error: Unexpected error during OpenAI generation: {type(e).__name__} - {e} ---"
|
158 |
+
print(error_msg); traceback.print_exc(); yield error_msg
|
retriever.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# services/retriever.py
|
2 |
+
# Keep this file exactly as it was in the previous correct version.
|
3 |
+
# It correctly uses config and utils.
|
4 |
+
import time
|
5 |
+
import traceback
|
6 |
+
from typing import List, Dict, Optional, Tuple
|
7 |
+
from pinecone import Pinecone, Index
|
8 |
+
from langsmith import traceable
|
9 |
+
|
10 |
+
try:
|
11 |
+
import config
|
12 |
+
from utils import get_embedding
|
13 |
+
except ImportError:
|
14 |
+
print("Error: Failed to import config or utils in retriever.py")
|
15 |
+
raise SystemExit("Failed imports in retriever.py")
|
16 |
+
|
17 |
+
# --- Globals ---
|
18 |
+
pinecone_client: Optional[Pinecone] = None
|
19 |
+
pinecone_index: Optional[Index] = None
|
20 |
+
is_retriever_ready: bool = False
|
21 |
+
retriever_status_message: str = "Retriever not initialized."
|
22 |
+
|
23 |
+
# --- Initialization ---
|
24 |
+
def init_retriever() -> Tuple[bool, str]:
|
25 |
+
"""Initializes the Pinecone client and index connection."""
|
26 |
+
global pinecone_client, pinecone_index, is_retriever_ready, retriever_status_message
|
27 |
+
if is_retriever_ready: return True, retriever_status_message
|
28 |
+
if not config.PINECONE_API_KEY:
|
29 |
+
retriever_status_message = "Error: PINECONE_API_KEY not found in Secrets."
|
30 |
+
is_retriever_ready = False; return False, retriever_status_message
|
31 |
+
if not config.OPENAI_API_KEY:
|
32 |
+
retriever_status_message = "Error: OPENAI_API_KEY not found (needed for query embeddings)."
|
33 |
+
is_retriever_ready = False; return False, retriever_status_message
|
34 |
+
try:
|
35 |
+
print("Retriever: Initializing Pinecone client...")
|
36 |
+
pinecone_client = Pinecone(api_key=config.PINECONE_API_KEY)
|
37 |
+
index_name = config.PINECONE_INDEX_NAME
|
38 |
+
print(f"Retriever: Checking for Pinecone index '{index_name}'...")
|
39 |
+
available_indexes = [idx.name for idx in pinecone_client.list_indexes().indexes]
|
40 |
+
if index_name not in available_indexes:
|
41 |
+
retriever_status_message = f"Error: Pinecone index '{index_name}' does not exist."
|
42 |
+
is_retriever_ready = False; pinecone_client = None; return False, retriever_status_message
|
43 |
+
print(f"Retriever: Connecting to Pinecone index '{index_name}'...")
|
44 |
+
pinecone_index = pinecone_client.Index(index_name)
|
45 |
+
stats = pinecone_index.describe_index_stats()
|
46 |
+
print(f"Retriever: Pinecone index stats: {stats}")
|
47 |
+
if stats.total_vector_count == 0:
|
48 |
+
retriever_status_message = f"Retriever connected, but index '{index_name}' is empty."
|
49 |
+
else:
|
50 |
+
retriever_status_message = f"Retriever ready (Index: {index_name}, Embed Model: {config.EMBEDDING_MODEL})."
|
51 |
+
is_retriever_ready = True
|
52 |
+
return True, retriever_status_message
|
53 |
+
except Exception as e:
|
54 |
+
error_msg = f"Error initializing Pinecone: {type(e).__name__} - {e}"; print(error_msg); traceback.print_exc()
|
55 |
+
retriever_status_message = error_msg; is_retriever_ready = False; pinecone_client = None; pinecone_index = None
|
56 |
+
return False, retriever_status_message
|
57 |
+
|
58 |
+
def get_retriever_status() -> Tuple[bool, str]:
|
59 |
+
if not is_retriever_ready: init_retriever()
|
60 |
+
return is_retriever_ready, retriever_status_message
|
61 |
+
|
62 |
+
# --- Core Function ---
|
63 |
+
@traceable(name="pinecone-retrieve-documents")
|
64 |
+
def retrieve_documents(query_text: str, n_results: int) -> List[Dict]:
|
65 |
+
global pinecone_index
|
66 |
+
ready, message = get_retriever_status()
|
67 |
+
if not ready or pinecone_index is None:
|
68 |
+
print(f"Retriever not ready: {message}"); return []
|
69 |
+
print(f"Retriever: Retrieving top {n_results} docs for query: '{query_text[:100]}...'"); start_time = time.time()
|
70 |
+
try:
|
71 |
+
query_embedding = get_embedding(query_text, model=config.EMBEDDING_MODEL)
|
72 |
+
if query_embedding is None: print("Retriever: Failed query embedding."); return []
|
73 |
+
response = pinecone_index.query(vector=query_embedding, top_k=n_results, include_metadata=True)
|
74 |
+
formatted_results = []
|
75 |
+
if not response or not response.matches: print("Retriever: No results found."); return []
|
76 |
+
for match in response.matches:
|
77 |
+
metadata = match.metadata if match.metadata else {}
|
78 |
+
doc_data = {
|
79 |
+
"vector_id": match.id, "original_id": metadata.get('original_id', match.id),
|
80 |
+
"source_name": metadata.get('source_name', 'Unknown Source'),
|
81 |
+
"hebrew_text": metadata.get('hebrew_text', ''), "english_text": metadata.get('english_text', ''),
|
82 |
+
"similarity_score": match.score, 'metadata_raw': metadata
|
83 |
+
}
|
84 |
+
formatted_results.append(doc_data)
|
85 |
+
total_time = time.time() - start_time; print(f"Retriever: Retrieved {len(formatted_results)} docs in {total_time:.2f}s.")
|
86 |
+
return formatted_results
|
87 |
+
except Exception as e:
|
88 |
+
print(f"Retriever: Error during query/processing: {type(e).__name__}"); traceback.print_exc(); return []
|