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:
Scott Idem
2026-05-09 13:38:38 -04:00
parent b9a78819ac
commit 7b443b40a4
9 changed files with 244 additions and 26 deletions

View File

@@ -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",