mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
Add API timeout to all backends + mesh-aware system prompt
All three LLM backends (Google, OpenAI, Anthropic) now wrap API calls in asyncio.wait_for() using config.timeout (default 30s). Previously Gemini could hang indefinitely with grounding+AFC enabled. Router catches TimeoutError with user-friendly "request timed out" message. Empty context buffer now injects "[No recent mesh traffic observed yet.]" so the LLM knows the capability exists even when buffer is empty. Default system prompt updated to mention mesh awareness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a01c9b2d05
commit
1172b9b67f
5 changed files with 54 additions and 17 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
"""Anthropic (Claude) LLM backend with rolling summary memory."""
|
"""Anthropic (Claude) LLM backend with rolling summary memory."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -114,17 +115,23 @@ class AnthropicBackend(LLMBackend):
|
||||||
final_messages = messages
|
final_messages = messages
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self._client.messages.create(
|
response = await asyncio.wait_for(
|
||||||
|
self._client.messages.create(
|
||||||
model=self.config.model,
|
model=self.config.model,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
system=enhanced_system,
|
system=enhanced_system,
|
||||||
messages=final_messages,
|
messages=final_messages,
|
||||||
|
),
|
||||||
|
timeout=self.config.timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract text from response
|
# Extract text from response
|
||||||
content = response.content[0].text if response.content else ""
|
content = response.content[0].text if response.content else ""
|
||||||
return content.strip()
|
return content.strip()
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error(f"Anthropic API timed out after {self.config.timeout}s")
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Anthropic API error: {e}")
|
logger.error(f"Anthropic API error: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Google Gemini LLM backend with rolling summary memory and Google Search grounding."""
|
"""Google Gemini LLM backend with rolling summary memory and Google Search grounding."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -53,15 +54,20 @@ class GoogleBackend(LLMBackend):
|
||||||
prompt = _SUMMARIZE_PROMPT.format(conversation=conversation)
|
prompt = _SUMMARIZE_PROMPT.format(conversation=conversation)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self._client.aio.models.generate_content(
|
response = await asyncio.wait_for(
|
||||||
|
self._client.aio.models.generate_content(
|
||||||
model=self.config.model,
|
model=self.config.model,
|
||||||
contents=prompt,
|
contents=prompt,
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig(
|
||||||
max_output_tokens=150,
|
max_output_tokens=150,
|
||||||
temperature=0.3,
|
temperature=0.3,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
timeout=self.config.timeout,
|
||||||
)
|
)
|
||||||
return response.text.strip() if response.text else f"Previous conversation: {len(messages)} messages."
|
return response.text.strip() if response.text else f"Previous conversation: {len(messages)} messages."
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning(f"Summary generation timed out after {self.config.timeout}s")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to generate summary: {e}")
|
logger.warning(f"Failed to generate summary: {e}")
|
||||||
return f"Previous conversation: {len(messages)} messages about various topics."
|
return f"Previous conversation: {len(messages)} messages about various topics."
|
||||||
|
|
@ -112,14 +118,20 @@ class GoogleBackend(LLMBackend):
|
||||||
tools=tools if tools else None,
|
tools=tools if tools else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await self._client.aio.models.generate_content(
|
response = await asyncio.wait_for(
|
||||||
|
self._client.aio.models.generate_content(
|
||||||
model=self.config.model,
|
model=self.config.model,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
config=config,
|
config=config,
|
||||||
|
),
|
||||||
|
timeout=self.config.timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response.text.strip() if response.text else ""
|
return response.text.strip() if response.text else ""
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error(f"Google API timed out after {self.config.timeout}s")
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Google API error: {e}")
|
logger.error(f"Google API error: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""OpenAI-compatible LLM backend with rolling summary memory."""
|
"""OpenAI-compatible LLM backend with rolling summary memory."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -134,11 +135,17 @@ class OpenAIBackend(LLMBackend):
|
||||||
if getattr(self.config, 'web_search', False):
|
if getattr(self.config, 'web_search', False):
|
||||||
request_kwargs["extra_body"] = {"features": {"web_search": True}}
|
request_kwargs["extra_body"] = {"features": {"web_search": True}}
|
||||||
|
|
||||||
response = await self._client.chat.completions.create(**request_kwargs)
|
response = await asyncio.wait_for(
|
||||||
|
self._client.chat.completions.create(**request_kwargs),
|
||||||
|
timeout=self.config.timeout,
|
||||||
|
)
|
||||||
|
|
||||||
content = response.choices[0].message.content
|
content = response.choices[0].message.content
|
||||||
return content.strip() if content else ""
|
return content.strip() if content else ""
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error(f"OpenAI API timed out after {self.config.timeout}s")
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"OpenAI API error: {e}")
|
logger.error(f"OpenAI API error: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,10 @@ class LLMConfig:
|
||||||
system_prompt: str = (
|
system_prompt: str = (
|
||||||
"You are a helpful assistant on a Meshtastic mesh network. "
|
"You are a helpful assistant on a Meshtastic mesh network. "
|
||||||
"Keep responses VERY brief - under 250 characters total. "
|
"Keep responses VERY brief - under 250 characters total. "
|
||||||
"Be concise but friendly. No markdown formatting."
|
"Be concise but friendly. No markdown formatting. "
|
||||||
|
"You can passively observe recent mesh traffic when available. "
|
||||||
|
"If asked about mesh activity and no recent traffic is shown below, "
|
||||||
|
"say you haven't observed any traffic yet rather than claiming you lack access."
|
||||||
)
|
)
|
||||||
use_system_prompt: bool = True # Toggle to disable sending system prompt
|
use_system_prompt: bool = True # Toggle to disable sending system prompt
|
||||||
web_search: bool = False # Enable web search (Open WebUI feature)
|
web_search: bool = False # Enable web search (Open WebUI feature)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Message routing logic for MeshAI."""
|
"""Message routing logic for MeshAI."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -159,6 +160,10 @@ class MessageRouter:
|
||||||
"\n\n--- Recent mesh traffic (for context only, not messages to you) ---\n"
|
"\n\n--- Recent mesh traffic (for context only, not messages to you) ---\n"
|
||||||
+ context_block
|
+ context_block
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
system_prompt += (
|
||||||
|
"\n\n[No recent mesh traffic observed yet.]"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self.llm.generate(
|
response = await self.llm.generate(
|
||||||
|
|
@ -166,6 +171,9 @@ class MessageRouter:
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
max_tokens=500,
|
max_tokens=500,
|
||||||
)
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error("LLM request timed out")
|
||||||
|
response = "Sorry, request timed out. Try again."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"LLM generation error: {e}")
|
logger.error(f"LLM generation error: {e}")
|
||||||
response = "Sorry, I encountered an error. Please try again."
|
response = "Sorry, I encountered an error. Please try again."
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue