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

Upload 3 files

Browse files
Files changed (3) hide show
  1. _initi_.py +2 -0
  2. openai_service.py +158 -0
  3. 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 []