syntaxhacker commited on
Commit
af4937c
Β·
1 Parent(s): 87285c1

use preciz SDK from pip package instead of local path

Browse files

- Import _call_llm, _parse_tool_calls, execute_tool, _strip_tool_tags
from pip-installed preciz (summarizer.llm_client)
- Remove sys.path.insert hack for local redisum
- Register rag_qa handler in TOOL_HANDLERS at module level
- Add scripts/utils_shim.py + Dockerfile step to work around
missing utils module in preciz pip package
- Update requirements.txt with preciz git dependency
- Fix system prompt to match parser's expected XML format
(name wrapped in <longcat_arg_key>)

Dockerfile CHANGED
@@ -8,7 +8,10 @@ WORKDIR /app
8
 
9
  # Copy requirements and install dependencies
10
  COPY --chown=user requirements.txt .
11
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
12
 
13
  # Copy application code
14
  COPY --chown=user app/ app/
 
8
 
9
  # Copy requirements and install dependencies
10
  COPY --chown=user requirements.txt .
11
+ COPY --chown=user scripts/ scripts/
12
+ RUN pip install --no-cache-dir -r requirements.txt && \
13
+ python -c "import site; import shutil; shutil.copy('scripts/utils_shim.py', f'{site.getsitepackages()[0]}/utils.py')" && \
14
+ rm -rf scripts
15
 
16
  # Copy application code
17
  COPY --chown=user app/ app/
app/main.py CHANGED
@@ -3,138 +3,56 @@ from pydantic import BaseModel
3
  import os
4
  import logging
5
  import sys
 
6
  from dotenv import load_dotenv
7
  from .config import DATASET_CONFIGS, load_prompt_template
8
- from openai import OpenAI
9
- from openai.types.chat import ChatCompletionMessageParam
10
- import json
11
- import re
12
 
13
- # Load environment variables
14
  load_dotenv()
15
 
16
- def parse_xml_tool_calls(content: str) -> list[dict]:
17
- """Parse <longcat_tool_call> XML tags from model output into tool call dicts."""
18
- calls = []
19
- pattern = r'<longcat_tool_call>\s*(\w+)\s*(.*?)</longcat_tool_call>'
20
- for match in re.finditer(pattern, content, re.DOTALL):
21
- name = match.group(1)
22
- body = match.group(2).strip()
23
- args = {}
24
- pair_pattern = r'<longcat_arg_key>(\w+)</longcat_arg_key>\s*(?:<longcat_arg_value>(.*?)</longcat_arg_value>)?'
25
- for pair_match in re.finditer(pair_pattern, body, re.DOTALL):
26
- key = pair_match.group(1)
27
- value = pair_match.group(2)
28
- if value is not None:
29
- args[key] = value.strip()
30
- calls.append({"name": name, **args})
31
- return calls
32
-
33
- # Lazy imports to avoid blocking startup
34
- # from .pipeline import RAGPipeline # Will import when needed
35
- # import umap # Will import when needed for visualization
36
- # import plotly.express as px # Will import when needed for visualization
37
- # import plotly.graph_objects as go # Will import when needed for visualization
38
- # from plotly.subplots import make_subplots # Will import when needed for visualization
39
- # import numpy as np # Will import when needed for visualization
40
- # from sklearn.preprocessing import normalize # Will import when needed for visualization
41
- # import pandas as pd # Will import when needed for visualization
42
 
43
- # Configure logging
44
  logging.basicConfig(
45
  level=logging.INFO,
46
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
47
- handlers=[
48
- logging.StreamHandler(sys.stdout)
49
- ]
50
  )
51
  logger = logging.getLogger(__name__)
52
 
53
  app = FastAPI(title="RAG Pipeline API", description="Multi-dataset RAG API", version="1.0.0")
54
 
55
- # Initialize OpenRouter client
56
- openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
57
- if not openrouter_api_key:
58
- raise ValueError("OPENROUTER_API_KEY environment variable is not set")
59
-
60
- openrouter_client = OpenAI(
61
- base_url="https://openrouter.ai/api/v1",
62
- api_key=openrouter_api_key
63
- )
64
-
65
- # Model configuration
66
  MODEL_NAME = os.getenv("MODEL_NAME", "openrouter/owl-alpha")
 
67
 
68
- # Initialize pipelines for all datasets
69
  pipelines = {}
70
 
71
- logger.info(f"Starting RAG Pipeline API")
72
- logger.info(f"Port from env: {os.getenv('PORT', 'Not set - will use 8000')}")
73
  logger.info(f"Available datasets: {list(DATASET_CONFIGS.keys())}")
74
 
75
- # Define tools for the GLM model
76
  def rag_qa(question: str, dataset: str = "developer-portfolio") -> str:
77
- """
78
- Get answers from the RAG pipeline for specific questions about the dataset.
79
-
80
- Args:
81
- question: The question to answer using the RAG pipeline
82
- dataset: The dataset to search in (default: developer-portfolio)
83
-
84
- Returns:
85
- Answer from the RAG pipeline
86
- """
87
  try:
88
- # Check if pipelines are loaded
89
  if not pipelines:
90
- return "RAG Pipeline is running but datasets are still loading in the background. Please try again in a moment."
91
-
92
- # Select the appropriate pipeline based on dataset
93
  if dataset not in pipelines:
94
  return f"Dataset '{dataset}' not available. Available datasets: {list(pipelines.keys())}"
95
-
96
- selected_pipeline = pipelines[dataset]
97
- answer = selected_pipeline.answer_question(question)
98
- return answer
99
  except Exception as e:
100
  return f"Error accessing RAG pipeline: {str(e)}"
101
 
102
- # Tool definitions for GLM
103
- TOOLS = [
104
- {
105
- "type": "function",
106
- "function": {
107
- "name": "rag_qa",
108
- "description": "Get answers from the RAG pipeline for specific questions about datasets",
109
- "parameters": {
110
- "type": "object",
111
- "properties": {
112
- "question": {
113
- "type": "string",
114
- "description": "The question to answer using the RAG pipeline"
115
- },
116
- "dataset": {
117
- "type": "string",
118
- "description": "The dataset to search in (default: developer-portfolio)",
119
- "default": "developer-portfolio"
120
- }
121
- },
122
- "required": ["question"]
123
- }
124
- }
125
- }
126
- ]
127
-
128
- # Don't load datasets during startup - do it asynchronously after server starts
129
- logger.info("RAG Pipeline API is ready to serve requests - datasets will load in background")
130
 
131
- # Visualization function disabled to speed up startup
132
- # def create_3d_visualization(pipeline):
133
- # ... (commented out for faster startup)
134
 
135
  class Question(BaseModel):
136
  text: str
137
- dataset: str = "developer-portfolio" # Default dataset
138
 
139
  class ChatMessage(BaseModel):
140
  role: str
@@ -142,151 +60,56 @@ class ChatMessage(BaseModel):
142
 
143
  class ChatRequest(BaseModel):
144
  messages: list[ChatMessage]
145
- dataset: str = "developer-portfolio" # Default dataset
146
 
147
  @app.post("/chat")
148
  async def chat_with_ai(request: ChatRequest):
149
- """
150
- Chat with the AI assistant. The AI will use the RAG pipeline when needed to answer questions about the datasets.
151
- """
152
- try:
153
- # Convert messages to OpenAI format with proper typing
154
- messages: list[ChatCompletionMessageParam] = [
155
- {"role": msg.role, "content": msg.content} # type: ignore
156
- for msg in request.messages
157
- ]
158
-
159
- # Add system message to guide the AI
160
- if request.dataset == "developer-portfolio":
161
- system_message: ChatCompletionMessageParam = {
162
- "role": "system",
163
- "content": load_prompt_template("system-instruction.txt")
164
- }
165
- else:
166
- system_message: ChatCompletionMessageParam = {
167
- "role": "system",
168
- "content": load_prompt_template("generic-system-instruction.txt")
169
- }
170
- messages.insert(0, system_message)
171
-
172
- # Make the API call with tools
173
- response = openrouter_client.chat.completions.create(
174
- model=MODEL_NAME,
175
- messages=messages,
176
- tools=TOOLS, # type: ignore
177
- tool_choice="auto"
178
- )
179
-
180
- message = response.choices[0].message
181
- finish_reason = response.choices[0].finish_reason
182
- content = message.content or ""
183
-
184
- # Check for native tool calls or XML-style tool calls in text
185
- xml_tool_calls = parse_xml_tool_calls(content)
186
- has_native_tool_calls = finish_reason == "tool_calls" and hasattr(message, 'tool_calls') and message.tool_calls
187
-
188
- if has_native_tool_calls or xml_tool_calls:
189
- tool_results = []
190
-
191
- if has_native_tool_calls:
192
- for tool_call in message.tool_calls:
193
- if tool_call.function and tool_call.function.name == "rag_qa":
194
- args = json.loads(tool_call.function.arguments or "{}")
195
- question = args.get("question")
196
- dataset = args.get("dataset", request.dataset)
197
- result = rag_qa(question, dataset)
198
- tool_results.append({"result": result, "native": True})
199
-
200
- assistant_message: ChatCompletionMessageParam = {
201
- "role": "assistant",
202
- "content": content,
203
- "tool_calls": [
204
- {
205
- "id": tc.id,
206
- "type": tc.type,
207
- "function": {
208
- "name": tc.function.name,
209
- "arguments": tc.function.arguments
210
- }
211
- }
212
- for tc in message.tool_calls
213
- if tc.function
214
- ]
215
- }
216
- messages.append(assistant_message)
217
-
218
- for tr in tool_results:
219
- messages.append({
220
- "role": "tool",
221
- "tool_call_id": "call_1",
222
- "content": tr["result"]
223
- })
224
-
225
- if xml_tool_calls:
226
- clean_content = re.sub(r'<longcat_tool_call>.*?</longcat_tool_call>', '', content, flags=re.DOTALL).strip()
227
- for tc in xml_tool_calls:
228
- if tc["name"] == "rag_qa":
229
- result = rag_qa(tc.get("question", ""), tc.get("dataset", request.dataset))
230
- tool_results.append({"result": result, "native": False})
231
-
232
- messages.append({
233
- "role": "assistant",
234
- "content": clean_content or "Let me look that up..."
235
- })
236
-
237
- rag_summary = "\n\n".join([tr["result"] for tr in tool_results])
238
- messages.append({
239
- "role": "user",
240
- "content": f"Here is the retrieved information:\n{rag_summary}\n\nNow provide a helpful answer based on this information."
241
- })
242
-
243
- final_response = openrouter_client.chat.completions.create(
244
- model=MODEL_NAME,
245
- messages=messages
246
- )
247
-
248
- return {
249
- "response": final_response.choices[0].message.content,
250
- "tool_calls": xml_tool_calls or [
251
- {
252
- "name": tc.function.name,
253
- "arguments": tc.function.arguments
254
- }
255
- for tc in message.tool_calls
256
- ] if has_native_tool_calls else None
257
- }
258
- else:
259
- return {
260
- "response": content,
261
- "tool_calls": None
262
- }
263
-
264
- except Exception as e:
265
- raise HTTPException(status_code=500, detail=str(e))
266
 
267
- # /answer endpoint removed - use /chat for all interactions
 
 
268
 
269
  @app.get("/datasets")
270
  async def list_datasets():
271
- """List all available datasets"""
272
  return {"datasets": list(pipelines.keys())}
273
 
274
  @app.get("/questions")
275
  async def list_questions(dataset: str = "developer-portfolio"):
276
- """List all questions for a given dataset"""
277
  if dataset not in pipelines:
278
  raise HTTPException(status_code=400, detail=f"Dataset '{dataset}' not available. Available datasets: {list(pipelines.keys())}")
279
-
280
  selected_pipeline = pipelines[dataset]
281
  questions = [doc.meta['question'] for doc in selected_pipeline.documents if 'question' in doc.meta]
282
  return {"dataset": dataset, "questions": questions}
283
 
284
  async def load_datasets_background():
285
- """Load datasets in background after server starts"""
286
  global pipelines
287
- # Import RAGPipeline only when needed
288
  from .pipeline import RAGPipeline
289
- # Only load developer-portfolio to save memory
290
  dataset_name = "developer-portfolio"
291
  try:
292
  logger.info(f"Loading dataset: {dataset_name}")
@@ -295,14 +118,11 @@ async def load_datasets_background():
295
  logger.info(f"Successfully loaded {dataset_name}")
296
  except Exception as e:
297
  logger.error(f"Failed to load {dataset_name}: {e}")
298
- logger.info(f"Background loading complete - {len(pipelines)} datasets loaded")
299
 
300
  @app.on_event("startup")
301
  async def startup_event():
302
  logger.info("FastAPI application startup complete")
303
- logger.info(f"Server should be running on port: {os.getenv('PORT', '8000')}")
304
-
305
- # Start loading datasets in background (non-blocking)
306
  import asyncio
307
  asyncio.create_task(load_datasets_background())
308
 
@@ -312,18 +132,15 @@ async def shutdown_event():
312
 
313
  @app.get("/")
314
  async def root():
315
- """Root endpoint"""
316
  return {"status": "ok", "message": "RAG Pipeline API", "version": "1.0.0", "datasets": list(pipelines.keys())}
317
 
318
  @app.get("/health")
319
  async def health_check():
320
- """Health check endpoint"""
321
- logger.info("Health check called")
322
  loading_status = "complete" if "developer-portfolio" in pipelines else "loading"
323
  return {
324
- "status": "healthy",
325
- "datasets_loaded": len(pipelines),
326
- "total_datasets": 1, # Only loading developer-portfolio
327
  "loading_status": loading_status,
328
- "port": os.getenv('PORT', '8000')
329
  }
 
3
  import os
4
  import logging
5
  import sys
6
+ import json
7
  from dotenv import load_dotenv
8
  from .config import DATASET_CONFIGS, load_prompt_template
 
 
 
 
9
 
 
10
  load_dotenv()
11
 
12
+ from summarizer.llm_client import _call_llm, _parse_tool_calls, _strip_tool_tags, TOOL_HANDLERS, execute_tool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
 
14
  logging.basicConfig(
15
  level=logging.INFO,
16
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ handlers=[logging.StreamHandler(sys.stdout)]
 
 
18
  )
19
  logger = logging.getLogger(__name__)
20
 
21
  app = FastAPI(title="RAG Pipeline API", description="Multi-dataset RAG API", version="1.0.0")
22
 
 
 
 
 
 
 
 
 
 
 
 
23
  MODEL_NAME = os.getenv("MODEL_NAME", "openrouter/owl-alpha")
24
+ MAX_ROUNDS = 6
25
 
 
26
  pipelines = {}
27
 
28
+ logger.info(f"Starting RAG Pipeline API β€” model: {MODEL_NAME}")
 
29
  logger.info(f"Available datasets: {list(DATASET_CONFIGS.keys())}")
30
 
 
31
  def rag_qa(question: str, dataset: str = "developer-portfolio") -> str:
 
 
 
 
 
 
 
 
 
 
32
  try:
 
33
  if not pipelines:
34
+ return "RAG Pipeline is running but datasets are still loading. Please try again in a moment."
 
 
35
  if dataset not in pipelines:
36
  return f"Dataset '{dataset}' not available. Available datasets: {list(pipelines.keys())}"
37
+ return pipelines[dataset].answer_question(question)
 
 
 
38
  except Exception as e:
39
  return f"Error accessing RAG pipeline: {str(e)}"
40
 
41
+ def handle_rag_qa_tool(tool_input: str, user_id: str | None = None) -> str:
42
+ try:
43
+ args = json.loads(tool_input)
44
+ return rag_qa(args.get("question", ""), args.get("dataset", "developer-portfolio"))
45
+ except json.JSONDecodeError:
46
+ parts = tool_input.split(":", 1)
47
+ if len(parts) == 2:
48
+ return rag_qa(parts[1].strip(), parts[0].strip())
49
+ return rag_qa(tool_input.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ TOOL_HANDLERS["rag_qa"] = handle_rag_qa_tool
 
 
52
 
53
  class Question(BaseModel):
54
  text: str
55
+ dataset: str = "developer-portfolio"
56
 
57
  class ChatMessage(BaseModel):
58
  role: str
 
60
 
61
  class ChatRequest(BaseModel):
62
  messages: list[ChatMessage]
63
+ dataset: str = "developer-portfolio"
64
 
65
  @app.post("/chat")
66
  async def chat_with_ai(request: ChatRequest):
67
+ messages = [{"role": m.role, "content": m.content} for m in request.messages]
68
+
69
+ if request.dataset == "developer-portfolio":
70
+ system = {"role": "system", "content": load_prompt_template("system-instruction.txt")}
71
+ else:
72
+ system = {"role": "system", "content": load_prompt_template("generic-system-instruction.txt")}
73
+ messages.insert(0, system)
74
+
75
+ for _ in range(MAX_ROUNDS):
76
+ content = _call_llm(messages, model=MODEL_NAME, max_tokens=4000)
77
+
78
+ tool_calls = _parse_tool_calls(content)
79
+ if not tool_calls:
80
+ clean = _strip_tool_tags(content)
81
+ return {"response": clean if clean else content, "tool_calls": None}
82
+
83
+ clean_content = _strip_tool_tags(content)
84
+ messages.append({"role": "assistant", "content": clean_content or "Let me check that..."})
85
+
86
+ results = []
87
+ for name, inp in tool_calls:
88
+ result = execute_tool(name, inp)
89
+ results.append(result)
90
+
91
+ for result in results:
92
+ messages.append({"role": "user", "content": f"RAG result:\n{result}\n\nNow answer based on this."})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ content = _call_llm(messages, model=MODEL_NAME, max_tokens=4000)
95
+ clean = _strip_tool_tags(content)
96
+ return {"response": clean if clean else content, "tool_calls": None}
97
 
98
  @app.get("/datasets")
99
  async def list_datasets():
 
100
  return {"datasets": list(pipelines.keys())}
101
 
102
  @app.get("/questions")
103
  async def list_questions(dataset: str = "developer-portfolio"):
 
104
  if dataset not in pipelines:
105
  raise HTTPException(status_code=400, detail=f"Dataset '{dataset}' not available. Available datasets: {list(pipelines.keys())}")
 
106
  selected_pipeline = pipelines[dataset]
107
  questions = [doc.meta['question'] for doc in selected_pipeline.documents if 'question' in doc.meta]
108
  return {"dataset": dataset, "questions": questions}
109
 
110
  async def load_datasets_background():
 
111
  global pipelines
 
112
  from .pipeline import RAGPipeline
 
113
  dataset_name = "developer-portfolio"
114
  try:
115
  logger.info(f"Loading dataset: {dataset_name}")
 
118
  logger.info(f"Successfully loaded {dataset_name}")
119
  except Exception as e:
120
  logger.error(f"Failed to load {dataset_name}: {e}")
121
+ logger.info(f"Background loading complete β€” {len(pipelines)} datasets loaded")
122
 
123
  @app.on_event("startup")
124
  async def startup_event():
125
  logger.info("FastAPI application startup complete")
 
 
 
126
  import asyncio
127
  asyncio.create_task(load_datasets_background())
128
 
 
132
 
133
  @app.get("/")
134
  async def root():
 
135
  return {"status": "ok", "message": "RAG Pipeline API", "version": "1.0.0", "datasets": list(pipelines.keys())}
136
 
137
  @app.get("/health")
138
  async def health_check():
 
 
139
  loading_status = "complete" if "developer-portfolio" in pipelines else "loading"
140
  return {
141
+ "status": "healthy",
142
+ "datasets_loaded": len(pipelines),
143
+ "total_datasets": 1,
144
  "loading_status": loading_status,
145
+ "port": os.getenv("PORT", "8000"),
146
  }
prompts/system-instruction.txt CHANGED
@@ -13,8 +13,12 @@ When you use rag_qa tool, you MUST use retrieved information to answer about the
13
  - πŸ“ Format responses with markdown for readability
14
 
15
  ## πŸ›  Tool Calling Format
16
- If you don't support native function calling, use this XML format to call the rag_qa tool:
17
- <longcat_tool_call>rag_qa <longcat_arg_key>question</longcat_arg_key> <longcat_arg_value>your question here</longcat_arg_value> <longcat_arg_key>dataset</longcat_arg_key> <longcat_arg_value>developer-portfolio</longcat_arg_value> </longcat_tool_call>
 
 
 
 
18
 
19
  ## βœ… Examples:
20
  ❌ **Wrong:** "I am a Tech Lead at FleetEnable"
 
13
  - πŸ“ Format responses with markdown for readability
14
 
15
  ## πŸ›  Tool Calling Format
16
+ Use the rag_qa tool to retrieve information when needed. Output the tool call in this exact XML format β€” each piece on its own line, with `name` wrapped in `<longcat_arg_key>`:
17
+ <longcat_tool_call>
18
+ <longcat_arg_key>name</longcat_arg_key><longcat_arg_value>rag_qa</longcat_arg_value>
19
+ <longcat_arg_key>question</longcat_arg_key><longcat_arg_value>your question here</longcat_arg_value>
20
+ <longcat_arg_key>dataset</longcat_arg_key><longcat_arg_value>developer-portfolio</longcat_arg_value>
21
+ </longcat_tool_call>
22
 
23
  ## βœ… Examples:
24
  ❌ **Wrong:** "I am a Tech Lead at FleetEnable"
requirements.txt CHANGED
@@ -3,8 +3,9 @@ datasets==3.3.2
3
  onnxruntime==1.20.1
4
  transformers==4.46.3
5
  huggingface-hub>=0.24.0
6
- fastapi==0.115.4
7
- uvicorn==0.31.0
8
- openai==1.57.0
9
- python-dotenv==1.0.1
10
- pydantic==2.10.4
 
 
3
  onnxruntime==1.20.1
4
  transformers==4.46.3
5
  huggingface-hub>=0.24.0
6
+ fastapi>=0.115.4
7
+ uvicorn>=0.31.0
8
+ openai>=1.57.0
9
+ python-dotenv>=1.0.1
10
+ pydantic>=2.10.4
11
+ preciz @ git+https://github.com/syntaxhacker/preciz-agent.git@cli-sdk
scripts/utils_shim.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, tempfile
2
+
3
+ _DATA_DIR = os.environ.get("PRECIZ_DATA_DIR", "/tmp")
4
+
5
+
6
+ def _data_dir(username=None):
7
+ d = _DATA_DIR
8
+ if username:
9
+ d = os.path.join(d, username)
10
+ return d
11
+
12
+
13
+ def load_memory():
14
+ p = os.path.join(_data_dir(), "memory", "memory.json")
15
+ os.makedirs(os.path.dirname(p), exist_ok=True)
16
+ if not os.path.exists(p):
17
+ return {"ttl_days": 7, "model_cache": {}}
18
+ try:
19
+ with open(p) as f:
20
+ return json.load(f)
21
+ except json.JSONDecodeError:
22
+ return {"ttl_days": 7, "model_cache": {}}
23
+
24
+
25
+ def save_memory(memory):
26
+ p = os.path.join(_data_dir(), "memory", "memory.json")
27
+ os.makedirs(os.path.dirname(p), exist_ok=True)
28
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(p), suffix=".json")
29
+ try:
30
+ with os.fdopen(fd, "w") as f:
31
+ json.dump(memory, f, indent=2)
32
+ os.replace(tmp, p)
33
+ except Exception:
34
+ try:
35
+ os.unlink(tmp)
36
+ except Exception:
37
+ pass
38
+ raise