Nexari-Research commited on
Commit
4920de9
·
verified ·
1 Parent(s): 160daf0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -72
app.py CHANGED
@@ -1,19 +1,24 @@
1
- # app.py -- robust SSE status handling and chunk sanitization
2
- # + system identity prompt (Nexari-G1, creator: Piyush)
3
  import os
4
  import json
5
  import logging
6
  import asyncio
7
- from fastapi import FastAPI
8
  from fastapi.responses import StreamingResponse
9
  from pydantic import BaseModel
10
  from typing import Any, Dict
11
 
12
- # Local model modules (expect these to exist in your project)
13
  import router_model
14
  import coder_model
15
  import chat_model
16
 
 
 
 
 
 
 
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("nexari.app")
19
 
@@ -38,15 +43,17 @@ async def startup_event():
38
  coder_model.BASE_DIR = os.path.join(MODEL_DIR, "coder")
39
  chat_model.BASE_DIR = os.path.join(MODEL_DIR, "chat")
40
 
 
41
  tasks = [
42
  asyncio.create_task(router_model.load_model_async()),
43
  asyncio.create_task(coder_model.load_model_async()),
44
  asyncio.create_task(chat_model.load_model_async()),
 
45
  ]
46
  results = await asyncio.gather(*tasks, return_exceptions=True)
47
  for i, r in enumerate(results):
48
  if isinstance(r, Exception):
49
- logger.error("Model loader %d failed: %s", i, r)
50
  logger.info("Startup complete.")
51
 
52
  class Message(BaseModel):
@@ -57,57 +64,26 @@ class ChatRequest(BaseModel):
57
  messages: list[Message]
58
  stream: bool = True
59
  temperature: float = 0.7
60
-
61
- def get_intent(last_user_message: str):
62
- # If router model missing, use a simple rule
63
- if not getattr(router_model, "model", None):
64
- text = (last_user_message or "").lower()
65
- if any(tok in text for tok in ["code", "bug", "fix", "error", "function", "python", "js", "html", "css"]):
66
- return "coding", "neutral"
67
- if any(tok in text for tok in ["why", "how", "prove", "reason", "think"]):
68
- return "reasoning", "neutral"
69
- return "chat", "neutral"
70
-
71
- sys_prompt = "Analyze intent. Return JSON like {'intent':'coding'|'chat'|'reasoning', 'sentiment':'neutral'|'sad'}"
72
- try:
73
- res = router_model.model.create_chat_completion(
74
- messages=[{"role":"system","content":sys_prompt},{"role":"user","content": last_user_message}],
75
- temperature=0.1, max_tokens=50
76
- )
77
- content = ""
78
- try:
79
- content = res['choices'][0]['message']['content'].lower()
80
- except Exception:
81
- try:
82
- content = res['choices'][0]['text'].lower()
83
- except Exception:
84
- content = ""
85
- if "coding" in content:
86
- return "coding", "neutral"
87
- if "reasoning" in content or "think" in content or "solve" in content:
88
- return "reasoning", "neutral"
89
- if "sad" in content:
90
- return "chat", "sad"
91
- return "chat", "neutral"
92
- except Exception as e:
93
- logger.exception("Router failure: %s", e)
94
- return "chat", "neutral"
95
 
96
  def sanitize_chunk(chunk: Any) -> Dict[str, Any]:
97
- """
98
- Ensure chunk is a JSON-serializable mapping for SSE.
99
- Remove any 'status' fields so we never send an unintended status overwrite.
100
- """
101
- # If chunk is already a dict-like
102
  if isinstance(chunk, dict):
103
- # shallow copy to avoid mutating model internals
104
  out = {}
105
  for k, v in chunk.items():
106
  if k == "status":
107
- # drop status fields from model-chunks; log for diagnostics
108
  logger.debug("Dropping status field from model chunk: %s", v)
109
  continue
110
- # try to keep strings and numbers; for complex objects convert to str
111
  if isinstance(v, (str, int, float, bool, type(None))):
112
  out[k] = v
113
  else:
@@ -118,15 +94,12 @@ def sanitize_chunk(chunk: Any) -> Dict[str, Any]:
118
  out[k] = str(v)
119
  return out
120
  else:
121
- # Not a dict: coerce into a safe dict with text key
122
  try:
123
- # if it's bytes or similar, convert
124
  txt = str(chunk)
125
  return {"text": txt}
126
  except Exception:
127
  return {"text": "[UNSERIALIZABLE_CHUNK]"}
128
 
129
- # Static system identity prefix to include in system prompts:
130
  SYSTEM_IDENTITY_PREFIX = (
131
  "You are Nexari-G1, an AI assistant created by Piyush (developer name: Piyush). "
132
  "always understand the user behaviour and request. "
@@ -137,23 +110,32 @@ SYSTEM_IDENTITY_PREFIX = (
137
 
138
  @app.post("/v1/chat/completions")
139
  async def chat_endpoint(request: ChatRequest):
140
- # Validate incoming
141
  messages = [m.dict() for m in request.messages] if request.messages else []
142
  if not messages:
143
- return {"error": "No messages provided."}
144
  last = messages[-1]['content']
145
 
146
- intent, sentiment = get_intent(last)
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  selected_model = None
149
- # base system message will always include identity prefix
150
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are a helpful assistant."
151
- status_indicator = "Thinking..." # default if not changed below
152
 
153
  if intent == "coding":
154
  if not getattr(coder_model, "model", None):
155
  logger.error("Coder model not available.")
156
- return {"error":"Coder model not available."}
157
  selected_model = coder_model.model
158
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are an expert Coding Assistant. Write clean, efficient code with comments where helpful."
159
  status_indicator = "Coding..."
@@ -161,7 +143,7 @@ async def chat_endpoint(request: ChatRequest):
161
  elif intent == "reasoning":
162
  if not getattr(chat_model, "model", None):
163
  logger.error("Chat model not available for reasoning.")
164
- return {"error":"Model not available."}
165
  selected_model = chat_model.model
166
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are a reasoning-focused assistant. Walk through your thinking clearly and show steps if relevant."
167
  status_indicator = "Reasoning..."
@@ -169,64 +151,76 @@ async def chat_endpoint(request: ChatRequest):
169
  else:
170
  if not getattr(chat_model, "model", None):
171
  logger.error("Chat model missing.")
172
- return {"error":"Chat model not available."}
173
  selected_model = chat_model.model
174
  logger.info("Intent: CHAT (%s)", sentiment)
175
  if sentiment == "sad":
176
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are empathic and calm. Provide supportive, concise responses."
177
  status_indicator = "Empathizing..."
178
  else:
179
- # default chat system message with identity included
180
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are a helpful conversational assistant."
181
 
182
- # ensure system prompt is present (first message)
183
  if messages[0].get("role") != "system":
184
  messages.insert(0, {"role":"system","content": sys_msg})
185
  else:
186
- # replace existing system content to ensure identity is present and consistent
187
  messages[0]["content"] = sys_msg
188
 
189
- # Streaming generator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  def iter_response():
191
  try:
192
- # 1) Send a single authoritative SSE status event (event: status)
193
- # Use event field so client can handle it separately from data token stream.
194
  status_payload = json.dumps({"status": status_indicator})
195
  event_payload = f"event: status\n"
196
  event_payload += f"data: {status_payload}\n\n"
197
  logger.info("Sending authoritative status event: %s", status_indicator)
198
  yield event_payload
199
 
200
- # 2) small flush hint to reduce buffering and help client parse promptly
201
  yield ":\n\n"
202
 
203
- # 3) Start streaming model output
204
  stream = selected_model.create_chat_completion(
205
  messages=messages,
206
  temperature=request.temperature,
207
  stream=True
208
  )
209
 
210
- # Iterate model generator and sanitize every chunk so it cannot inject a status
211
  for chunk in stream:
212
  safe = sanitize_chunk(chunk)
213
  try:
214
  yield f"data: {json.dumps(safe)}\n\n"
215
  except Exception:
216
- # fallback to a safe string representation
217
  yield f"data: {json.dumps({'text': str(safe)})}\n\n"
218
 
219
- # 4) final done marker
220
  yield "data: [DONE]\n\n"
221
  logger.info("Stream finished for request (status was: %s)", status_indicator)
222
 
223
  except Exception as e:
224
  logger.exception("Streaming error: %s", e)
225
- # send explicit error object
226
  try:
227
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
228
  except Exception:
229
  yield "data: {\"error\":\"streaming failure\"}\n\n"
230
  yield "data: [DONE]\n\n"
231
 
232
- return StreamingResponse(iter_response(), media_type="text/event-stream")
 
1
+ # app.py -- upgraded to use duckduckgo-based search and a lightweight local image generator
 
2
  import os
3
  import json
4
  import logging
5
  import asyncio
6
+ from fastapi import FastAPI, HTTPException
7
  from fastapi.responses import StreamingResponse
8
  from pydantic import BaseModel
9
  from typing import Any, Dict
10
 
11
+ # Local model modules
12
  import router_model
13
  import coder_model
14
  import chat_model
15
 
16
+ # New utilities
17
+ import intent_model
18
+ import web_search # duckduckgo-based wrapper
19
+ import image_gen # lightweight CPU image generator
20
+ from time_utils import parse_time_iso
21
+
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger("nexari.app")
24
 
 
43
  coder_model.BASE_DIR = os.path.join(MODEL_DIR, "coder")
44
  chat_model.BASE_DIR = os.path.join(MODEL_DIR, "chat")
45
 
46
+ # load models + intent model concurrently
47
  tasks = [
48
  asyncio.create_task(router_model.load_model_async()),
49
  asyncio.create_task(coder_model.load_model_async()),
50
  asyncio.create_task(chat_model.load_model_async()),
51
+ asyncio.create_task(intent_model.load_model_async()),
52
  ]
53
  results = await asyncio.gather(*tasks, return_exceptions=True)
54
  for i, r in enumerate(results):
55
  if isinstance(r, Exception):
56
+ logger.error("Loader %d failed: %s", i, r)
57
  logger.info("Startup complete.")
58
 
59
  class Message(BaseModel):
 
64
  messages: list[Message]
65
  stream: bool = True
66
  temperature: float = 0.7
67
+ # allow explicit user-requested tools (no API keys required)
68
+ use_web_search: bool = False
69
+ use_image_gen: bool = False
70
+ # optional: time parsing example parameter
71
+ time_hint: str = None
72
+ # optional image params (size/generate style) for future extension
73
+ image_params: dict = None
74
+
75
+ def get_intent_and_sentiment(last_user_message: str):
76
+ if not getattr(intent_model, "model", None):
77
+ raise RuntimeError("Intent model not loaded. Start-up failed or model missing.")
78
+ return intent_model.classify(last_user_message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  def sanitize_chunk(chunk: Any) -> Dict[str, Any]:
 
 
 
 
 
81
  if isinstance(chunk, dict):
 
82
  out = {}
83
  for k, v in chunk.items():
84
  if k == "status":
 
85
  logger.debug("Dropping status field from model chunk: %s", v)
86
  continue
 
87
  if isinstance(v, (str, int, float, bool, type(None))):
88
  out[k] = v
89
  else:
 
94
  out[k] = str(v)
95
  return out
96
  else:
 
97
  try:
 
98
  txt = str(chunk)
99
  return {"text": txt}
100
  except Exception:
101
  return {"text": "[UNSERIALIZABLE_CHUNK]"}
102
 
 
103
  SYSTEM_IDENTITY_PREFIX = (
104
  "You are Nexari-G1, an AI assistant created by Piyush (developer name: Piyush). "
105
  "always understand the user behaviour and request. "
 
110
 
111
  @app.post("/v1/chat/completions")
112
  async def chat_endpoint(request: ChatRequest):
 
113
  messages = [m.dict() for m in request.messages] if request.messages else []
114
  if not messages:
115
+ raise HTTPException(status_code=400, detail="No messages provided.")
116
  last = messages[-1]['content']
117
 
118
+ if request.time_hint:
119
+ try:
120
+ parsed = parse_time_iso(request.time_hint)
121
+ logger.info("Parsed user time_hint -> %s", parsed.isoformat())
122
+ except Exception as e:
123
+ logger.warning("time_hint parse failed: %s", e)
124
+
125
+ try:
126
+ intent, sentiment = get_intent_and_sentiment(last)
127
+ except Exception as e:
128
+ logger.exception("Intent detection failed: %s", e)
129
+ raise HTTPException(status_code=500, detail=f"Intent detection failed: {e}")
130
 
131
  selected_model = None
 
132
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are a helpful assistant."
133
+ status_indicator = "Thinking..."
134
 
135
  if intent == "coding":
136
  if not getattr(coder_model, "model", None):
137
  logger.error("Coder model not available.")
138
+ raise HTTPException(status_code=500, detail="Coder model not available.")
139
  selected_model = coder_model.model
140
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are an expert Coding Assistant. Write clean, efficient code with comments where helpful."
141
  status_indicator = "Coding..."
 
143
  elif intent == "reasoning":
144
  if not getattr(chat_model, "model", None):
145
  logger.error("Chat model not available for reasoning.")
146
+ raise HTTPException(status_code=500, detail="Chat model not available.")
147
  selected_model = chat_model.model
148
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are a reasoning-focused assistant. Walk through your thinking clearly and show steps if relevant."
149
  status_indicator = "Reasoning..."
 
151
  else:
152
  if not getattr(chat_model, "model", None):
153
  logger.error("Chat model missing.")
154
+ raise HTTPException(status_code=500, detail="Chat model not available.")
155
  selected_model = chat_model.model
156
  logger.info("Intent: CHAT (%s)", sentiment)
157
  if sentiment == "sad":
158
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are empathic and calm. Provide supportive, concise responses."
159
  status_indicator = "Empathizing..."
160
  else:
 
161
  sys_msg = SYSTEM_IDENTITY_PREFIX + "You are a helpful conversational assistant."
162
 
 
163
  if messages[0].get("role") != "system":
164
  messages.insert(0, {"role":"system","content": sys_msg})
165
  else:
 
166
  messages[0]["content"] = sys_msg
167
 
168
+ tool_context = {}
169
+ # Web search via duckduckgo (no API keys)
170
+ if request.use_web_search:
171
+ try:
172
+ logger.info("User requested web search for: %s", last)
173
+ snippets = web_search.search(last, max_results=3)
174
+ tool_context['web_search'] = snippets
175
+ messages.append({"role":"system","content": f"Web search results (top 3):\n{json.dumps(snippets)[:4000]}"})
176
+ except Exception as e:
177
+ logger.exception("Web search failed: %s", e)
178
+ messages.append({"role":"system","content": f"[Web search failed: {e}]"})
179
+ # Image generation using local CPU-friendly generator
180
+ if request.use_image_gen:
181
+ try:
182
+ logger.info("User requested image generation for: %s", last)
183
+ # call a synchronous local generator which returns metadata (path)
184
+ img_meta = image_gen.generate_image(prompt=last, params=request.image_params or {})
185
+ tool_context['image_result'] = img_meta
186
+ messages.append({"role":"system","content": f"Image generated: {json.dumps(img_meta)}"})
187
+ except Exception as e:
188
+ logger.exception("Image generation failed: %s", e)
189
+ messages.append({"role":"system","content": f"[Image generation failed: {e}]"})
190
+
191
+
192
  def iter_response():
193
  try:
 
 
194
  status_payload = json.dumps({"status": status_indicator})
195
  event_payload = f"event: status\n"
196
  event_payload += f"data: {status_payload}\n\n"
197
  logger.info("Sending authoritative status event: %s", status_indicator)
198
  yield event_payload
199
 
 
200
  yield ":\n\n"
201
 
 
202
  stream = selected_model.create_chat_completion(
203
  messages=messages,
204
  temperature=request.temperature,
205
  stream=True
206
  )
207
 
 
208
  for chunk in stream:
209
  safe = sanitize_chunk(chunk)
210
  try:
211
  yield f"data: {json.dumps(safe)}\n\n"
212
  except Exception:
 
213
  yield f"data: {json.dumps({'text': str(safe)})}\n\n"
214
 
 
215
  yield "data: [DONE]\n\n"
216
  logger.info("Stream finished for request (status was: %s)", status_indicator)
217
 
218
  except Exception as e:
219
  logger.exception("Streaming error: %s", e)
 
220
  try:
221
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
222
  except Exception:
223
  yield "data: {\"error\":\"streaming failure\"}\n\n"
224
  yield "data: [DONE]\n\n"
225
 
226
+ return StreamingResponse(iter_response(), media_type="text/event-stream")