feat: http_post tool, nc_talk_history tool, local orchestrator retry
- http_post: POST to external URLs with per-user URL prefix allowlist
(home/{user}/http_allowlist.json); admin-only, confirm-required
- nc_talk_history: read recent NC Talk messages via Basic Auth (requires
nc_username + nc_app_password in channels.json under nextcloud)
- openai_orchestrator: _chat_with_retry() wraps both API calls with
exponential backoff (3 attempts, 1s/2s) on connection errors and
transient status codes (429, 500, 502, 503, 504)
- Docs updated: CLAUDE.md, HELP.md, TODO, MASTER, ROADMAP (50 tools)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from openai import AsyncOpenAI, APIConnectionError, APIStatusError
|
||||
|
||||
from config import settings
|
||||
from orchestrator_engine import OrchestrateCheckpoint, OrchestratorResult
|
||||
@@ -286,7 +286,7 @@ async def _run_from_messages(
|
||||
if active_tools:
|
||||
call_kwargs["tools"] = active_tools
|
||||
call_kwargs["tool_choice"] = "auto"
|
||||
response = await client.chat.completions.create(**call_kwargs)
|
||||
response = await _chat_with_retry(client, **call_kwargs)
|
||||
|
||||
choice = response.choices[0]
|
||||
msg = choice.message
|
||||
@@ -345,7 +345,7 @@ async def _run_from_messages(
|
||||
conf_call: dict = {"model": model_name, "messages": messages, "tool_choice": "none"}
|
||||
if active_tools:
|
||||
conf_call["tools"] = active_tools
|
||||
conf_resp = await client.chat.completions.create(**conf_call)
|
||||
conf_resp = await _chat_with_retry(client, **conf_call)
|
||||
final_response = conf_resp.choices[0].message.content or (
|
||||
"This action requires your explicit confirmation before it can proceed."
|
||||
)
|
||||
@@ -386,6 +386,30 @@ async def _run_from_messages(
|
||||
return final_response, None
|
||||
|
||||
|
||||
_RETRY_STATUSES = {429, 500, 502, 503, 504}
|
||||
_MAX_API_RETRIES = 3
|
||||
|
||||
|
||||
async def _chat_with_retry(client, **kwargs):
|
||||
"""Wrap chat.completions.create with exponential backoff on transient errors."""
|
||||
last_exc: Exception = RuntimeError("No attempts made")
|
||||
for attempt in range(_MAX_API_RETRIES):
|
||||
try:
|
||||
return await client.chat.completions.create(**kwargs)
|
||||
except APIConnectionError as e:
|
||||
last_exc = e
|
||||
logger.warning("OpenAI connection error (attempt %d/%d): %s", attempt + 1, _MAX_API_RETRIES, e)
|
||||
except APIStatusError as e:
|
||||
if e.status_code in _RETRY_STATUSES:
|
||||
last_exc = e
|
||||
logger.warning("OpenAI status %d (attempt %d/%d): %s", e.status_code, attempt + 1, _MAX_API_RETRIES, e)
|
||||
else:
|
||||
raise
|
||||
if attempt < _MAX_API_RETRIES - 1:
|
||||
await asyncio.sleep(2 ** attempt) # 1s, 2s
|
||||
raise last_exc
|
||||
|
||||
|
||||
def _build_client(
|
||||
model_cfg: dict | None,
|
||||
user_role: str = "user",
|
||||
|
||||
Reference in New Issue
Block a user