mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
refactor: remove redundant local imports already available at module level
Sweep ~74 redundant local imports across 21 files where the same module was already imported at the top level. Also includes type fixes and lint cleanups on the same branch.
This commit is contained in:
parent
ce9c91c8f7
commit
1010e5fa3c
31 changed files with 289 additions and 316 deletions
|
|
@ -613,8 +613,8 @@ class HermesACPAgent(acp.Agent):
|
||||||
await self._conn.session_update(
|
await self._conn.session_update(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
update=AvailableCommandsUpdate(
|
update=AvailableCommandsUpdate(
|
||||||
sessionUpdate="available_commands_update",
|
session_update="available_commands_update",
|
||||||
availableCommands=self._available_commands(),
|
available_commands=self._available_commands(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -807,7 +807,7 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||||
)
|
)
|
||||||
self.summary_model = "" # empty = use main model
|
self.summary_model = "" # empty = use main model
|
||||||
self._summary_failure_cooldown_until = 0.0 # no cooldown
|
self._summary_failure_cooldown_until = 0.0 # no cooldown
|
||||||
return self._generate_summary(messages, summary_budget) # retry immediately
|
return self._generate_summary(turns_to_summarize) # retry immediately
|
||||||
|
|
||||||
# Transient errors (timeout, rate limit, network) — shorter cooldown
|
# Transient errors (timeout, rate limit, network) — shorter cooldown
|
||||||
_transient_cooldown = 60
|
_transient_cooldown = 60
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,8 @@ class CopilotACPClient:
|
||||||
stderr_tail: deque[str] = deque(maxlen=40)
|
stderr_tail: deque[str] = deque(maxlen=40)
|
||||||
|
|
||||||
def _stdout_reader() -> None:
|
def _stdout_reader() -> None:
|
||||||
|
if proc.stdout is None:
|
||||||
|
return
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
try:
|
try:
|
||||||
inbox.put(json.loads(line))
|
inbox.put(json.loads(line))
|
||||||
|
|
|
||||||
|
|
@ -799,7 +799,8 @@ def _gemini_http_error(response: httpx.Response) -> CodeAssistError:
|
||||||
err_obj = {}
|
err_obj = {}
|
||||||
err_status = str(err_obj.get("status") or "").strip()
|
err_status = str(err_obj.get("status") or "").strip()
|
||||||
err_message = str(err_obj.get("message") or "").strip()
|
err_message = str(err_obj.get("message") or "").strip()
|
||||||
err_details_list = err_obj.get("details") if isinstance(err_obj.get("details"), list) else []
|
_raw_details = err_obj.get("details")
|
||||||
|
err_details_list = _raw_details if isinstance(_raw_details, list) else []
|
||||||
|
|
||||||
# Extract google.rpc.ErrorInfo reason + metadata. There may be more
|
# Extract google.rpc.ErrorInfo reason + metadata. There may be more
|
||||||
# than one ErrorInfo (rare), so we pick the first one with a reason.
|
# than one ErrorInfo (rare), so we pick the first one with a reason.
|
||||||
|
|
|
||||||
|
|
@ -613,7 +613,8 @@ def gemini_http_error(response: httpx.Response) -> GeminiAPIError:
|
||||||
err_obj = {}
|
err_obj = {}
|
||||||
err_status = str(err_obj.get("status") or "").strip()
|
err_status = str(err_obj.get("status") or "").strip()
|
||||||
err_message = str(err_obj.get("message") or "").strip()
|
err_message = str(err_obj.get("message") or "").strip()
|
||||||
details_list = err_obj.get("details") if isinstance(err_obj.get("details"), list) else []
|
_raw_details = err_obj.get("details")
|
||||||
|
details_list = _raw_details if isinstance(_raw_details, list) else []
|
||||||
|
|
||||||
reason = ""
|
reason = ""
|
||||||
retry_after: Optional[float] = None
|
retry_after: Optional[float] = None
|
||||||
|
|
|
||||||
40
cli.py
40
cli.py
|
|
@ -529,7 +529,6 @@ def load_cli_config() -> Dict[str, Any]:
|
||||||
if _file_has_terminal_config or env_var not in os.environ:
|
if _file_has_terminal_config or env_var not in os.environ:
|
||||||
val = terminal_config[config_key]
|
val = terminal_config[config_key]
|
||||||
if isinstance(val, list):
|
if isinstance(val, list):
|
||||||
import json
|
|
||||||
os.environ[env_var] = json.dumps(val)
|
os.environ[env_var] = json.dumps(val)
|
||||||
else:
|
else:
|
||||||
os.environ[env_var] = str(val)
|
os.environ[env_var] = str(val)
|
||||||
|
|
@ -1144,8 +1143,6 @@ def _rich_text_from_ansi(text: str) -> _RichText:
|
||||||
|
|
||||||
def _strip_markdown_syntax(text: str) -> str:
|
def _strip_markdown_syntax(text: str) -> str:
|
||||||
"""Best-effort markdown marker removal for plain-text display."""
|
"""Best-effort markdown marker removal for plain-text display."""
|
||||||
import re
|
|
||||||
|
|
||||||
plain = _rich_text_from_ansi(text or "").plain
|
plain = _rich_text_from_ansi(text or "").plain
|
||||||
plain = re.sub(r"^\s{0,3}(?:[-*_]\s*){3,}$", "", plain, flags=re.MULTILINE)
|
plain = re.sub(r"^\s{0,3}(?:[-*_]\s*){3,}$", "", plain, flags=re.MULTILINE)
|
||||||
plain = re.sub(r"^\s{0,3}#{1,6}\s+", "", plain, flags=re.MULTILINE)
|
plain = re.sub(r"^\s{0,3}#{1,6}\s+", "", plain, flags=re.MULTILINE)
|
||||||
|
|
@ -2002,8 +1999,7 @@ class HermesCLI:
|
||||||
|
|
||||||
def _invalidate(self, min_interval: float = 0.25) -> None:
|
def _invalidate(self, min_interval: float = 0.25) -> None:
|
||||||
"""Throttled UI repaint — prevents terminal blinking on slow/SSH connections."""
|
"""Throttled UI repaint — prevents terminal blinking on slow/SSH connections."""
|
||||||
import time as _time
|
now = time.monotonic()
|
||||||
now = _time.monotonic()
|
|
||||||
if hasattr(self, "_app") and self._app and (now - self._last_invalidate) >= min_interval:
|
if hasattr(self, "_app") and self._app and (now - self._last_invalidate) >= min_interval:
|
||||||
self._last_invalidate = now
|
self._last_invalidate = now
|
||||||
self._app.invalidate()
|
self._app.invalidate()
|
||||||
|
|
@ -2221,8 +2217,7 @@ class HermesCLI:
|
||||||
return ""
|
return ""
|
||||||
t0 = getattr(self, "_tool_start_time", 0) or 0
|
t0 = getattr(self, "_tool_start_time", 0) or 0
|
||||||
if t0 > 0:
|
if t0 > 0:
|
||||||
import time as _time
|
elapsed = time.monotonic() - t0
|
||||||
elapsed = _time.monotonic() - t0
|
|
||||||
if elapsed >= 60:
|
if elapsed >= 60:
|
||||||
_m, _s = int(elapsed // 60), int(elapsed % 60)
|
_m, _s = int(elapsed // 60), int(elapsed % 60)
|
||||||
elapsed_str = f"{_m}m {_s}s"
|
elapsed_str = f"{_m}m {_s}s"
|
||||||
|
|
@ -2477,9 +2472,6 @@ class HermesCLI:
|
||||||
|
|
||||||
def _emit_reasoning_preview(self, reasoning_text: str) -> None:
|
def _emit_reasoning_preview(self, reasoning_text: str) -> None:
|
||||||
"""Render a buffered reasoning preview as a single [thinking] block."""
|
"""Render a buffered reasoning preview as a single [thinking] block."""
|
||||||
import re
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
preview_text = reasoning_text.strip()
|
preview_text = reasoning_text.strip()
|
||||||
if not preview_text:
|
if not preview_text:
|
||||||
return
|
return
|
||||||
|
|
@ -2598,9 +2590,7 @@ class HermesCLI:
|
||||||
"""Expand [Pasted text #N -> file] placeholders into file contents."""
|
"""Expand [Pasted text #N -> file] placeholders into file contents."""
|
||||||
if not isinstance(text, str) or "[Pasted text #" not in text:
|
if not isinstance(text, str) or "[Pasted text #" not in text:
|
||||||
return text or ""
|
return text or ""
|
||||||
import re as _re
|
paste_ref_re = re.compile(r'\[Pasted text #\d+: \d+ lines \u2192 (.+?)\]')
|
||||||
|
|
||||||
paste_ref_re = _re.compile(r'\[Pasted text #\d+: \d+ lines \u2192 (.+?)\]')
|
|
||||||
|
|
||||||
def _expand_ref(match):
|
def _expand_ref(match):
|
||||||
path = Path(match.group(1))
|
path = Path(match.group(1))
|
||||||
|
|
@ -2923,9 +2913,7 @@ class HermesCLI:
|
||||||
|
|
||||||
def _command_spinner_frame(self) -> str:
|
def _command_spinner_frame(self) -> str:
|
||||||
"""Return the current spinner frame for slow slash commands."""
|
"""Return the current spinner frame for slow slash commands."""
|
||||||
import time as _time
|
frame_idx = int(time.monotonic() * 10) % len(_COMMAND_SPINNER_FRAMES)
|
||||||
|
|
||||||
frame_idx = int(_time.monotonic() * 10) % len(_COMMAND_SPINNER_FRAMES)
|
|
||||||
return _COMMAND_SPINNER_FRAMES[frame_idx]
|
return _COMMAND_SPINNER_FRAMES[frame_idx]
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|
@ -3936,7 +3924,6 @@ class HermesCLI:
|
||||||
image later with ``vision_analyze`` if needed.
|
image later with ``vision_analyze`` if needed.
|
||||||
"""
|
"""
|
||||||
import asyncio as _asyncio
|
import asyncio as _asyncio
|
||||||
import json as _json
|
|
||||||
from tools.vision_tools import vision_analyze_tool
|
from tools.vision_tools import vision_analyze_tool
|
||||||
|
|
||||||
analysis_prompt = (
|
analysis_prompt = (
|
||||||
|
|
@ -3956,7 +3943,7 @@ class HermesCLI:
|
||||||
result_json = _asyncio.run(
|
result_json = _asyncio.run(
|
||||||
vision_analyze_tool(image_url=str(img_path), user_prompt=analysis_prompt)
|
vision_analyze_tool(image_url=str(img_path), user_prompt=analysis_prompt)
|
||||||
)
|
)
|
||||||
result = _json.loads(result_json)
|
result = json.loads(result_json)
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
description = result.get("analysis", "")
|
description = result.get("analysis", "")
|
||||||
enriched_parts.append(
|
enriched_parts.append(
|
||||||
|
|
@ -6282,8 +6269,7 @@ class HermesCLI:
|
||||||
# with the output (fixes #2718).
|
# with the output (fixes #2718).
|
||||||
if self._app:
|
if self._app:
|
||||||
self._app.invalidate()
|
self._app.invalidate()
|
||||||
import time as _tmod
|
time.sleep(0.05) # brief pause for refresh
|
||||||
_tmod.sleep(0.05) # brief pause for refresh
|
|
||||||
print()
|
print()
|
||||||
ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]")
|
ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]")
|
||||||
_cprint(f" ✅ Background task #{task_num} complete")
|
_cprint(f" ✅ Background task #{task_num} complete")
|
||||||
|
|
@ -6323,8 +6309,7 @@ class HermesCLI:
|
||||||
# Same TUI refresh pattern as success path (#2718)
|
# Same TUI refresh pattern as success path (#2718)
|
||||||
if self._app:
|
if self._app:
|
||||||
self._app.invalidate()
|
self._app.invalidate()
|
||||||
import time as _tmod
|
time.sleep(0.05)
|
||||||
_tmod.sleep(0.05)
|
|
||||||
print()
|
print()
|
||||||
_cprint(f" ❌ Background task #{task_num} failed: {e}")
|
_cprint(f" ❌ Background task #{task_num} failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -6544,7 +6529,6 @@ class HermesCLI:
|
||||||
_launched = self._try_launch_chrome_debug(_port, _plat.system())
|
_launched = self._try_launch_chrome_debug(_port, _plat.system())
|
||||||
if _launched:
|
if _launched:
|
||||||
# Wait for the port to come up
|
# Wait for the port to come up
|
||||||
import time as _time
|
|
||||||
for _wait in range(10):
|
for _wait in range(10):
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
@ -6554,7 +6538,7 @@ class HermesCLI:
|
||||||
_already_open = True
|
_already_open = True
|
||||||
break
|
break
|
||||||
except (OSError, socket.timeout):
|
except (OSError, socket.timeout):
|
||||||
_time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
if _already_open:
|
if _already_open:
|
||||||
print(f" ✓ Chrome launched and listening on port {_port}")
|
print(f" ✓ Chrome launched and listening on port {_port}")
|
||||||
else:
|
else:
|
||||||
|
|
@ -7084,7 +7068,6 @@ class HermesCLI:
|
||||||
known state. When a change is detected, triggers _reload_mcp() and
|
known state. When a change is detected, triggers _reload_mcp() and
|
||||||
informs the user so they know the tool list has been refreshed.
|
informs the user so they know the tool list has been refreshed.
|
||||||
"""
|
"""
|
||||||
import time
|
|
||||||
import yaml as _yaml
|
import yaml as _yaml
|
||||||
|
|
||||||
CONFIG_WATCH_INTERVAL = 5.0 # seconds between config.yaml stat() calls
|
CONFIG_WATCH_INTERVAL = 5.0 # seconds between config.yaml stat() calls
|
||||||
|
|
@ -7943,7 +7926,9 @@ class HermesCLI:
|
||||||
return
|
return
|
||||||
|
|
||||||
selected = state.get("selected", 0)
|
selected = state.get("selected", 0)
|
||||||
choices = state.get("choices") or []
|
choices = state.get("choices")
|
||||||
|
if not isinstance(choices, list):
|
||||||
|
choices = []
|
||||||
if not (0 <= selected < len(choices)):
|
if not (0 <= selected < len(choices)):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -10025,7 +10010,8 @@ class HermesCLI:
|
||||||
if stage == "provider":
|
if stage == "provider":
|
||||||
title = "⚙ Model Picker — Select Provider"
|
title = "⚙ Model Picker — Select Provider"
|
||||||
choices = []
|
choices = []
|
||||||
for p in state.get("providers") or []:
|
_providers = state.get("providers")
|
||||||
|
for p in _providers if isinstance(_providers, list) else []:
|
||||||
count = p.get("total_models", len(p.get("models", [])))
|
count = p.get("total_models", len(p.get("models", [])))
|
||||||
label = f"{p['name']} ({count} model{'s' if count != 1 else ''})"
|
label = f"{p['name']} ({count} model{'s' if count != 1 else ''})"
|
||||||
if p.get("is_current"):
|
if p.get("is_current"):
|
||||||
|
|
|
||||||
|
|
@ -670,8 +670,7 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
if "require_mention" in telegram_cfg and not os.getenv("TELEGRAM_REQUIRE_MENTION"):
|
if "require_mention" in telegram_cfg and not os.getenv("TELEGRAM_REQUIRE_MENTION"):
|
||||||
os.environ["TELEGRAM_REQUIRE_MENTION"] = str(telegram_cfg["require_mention"]).lower()
|
os.environ["TELEGRAM_REQUIRE_MENTION"] = str(telegram_cfg["require_mention"]).lower()
|
||||||
if "mention_patterns" in telegram_cfg and not os.getenv("TELEGRAM_MENTION_PATTERNS"):
|
if "mention_patterns" in telegram_cfg and not os.getenv("TELEGRAM_MENTION_PATTERNS"):
|
||||||
import json as _json
|
os.environ["TELEGRAM_MENTION_PATTERNS"] = json.dumps(telegram_cfg["mention_patterns"])
|
||||||
os.environ["TELEGRAM_MENTION_PATTERNS"] = _json.dumps(telegram_cfg["mention_patterns"])
|
|
||||||
frc = telegram_cfg.get("free_response_chats")
|
frc = telegram_cfg.get("free_response_chats")
|
||||||
if frc is not None and not os.getenv("TELEGRAM_FREE_RESPONSE_CHATS"):
|
if frc is not None and not os.getenv("TELEGRAM_FREE_RESPONSE_CHATS"):
|
||||||
if isinstance(frc, list):
|
if isinstance(frc, list):
|
||||||
|
|
@ -1259,7 +1258,6 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
if legacy_home:
|
if legacy_home:
|
||||||
qq_home = legacy_home
|
qq_home = legacy_home
|
||||||
qq_home_name_env = "QQ_HOME_CHANNEL_NAME"
|
qq_home_name_env = "QQ_HOME_CHANNEL_NAME"
|
||||||
import logging
|
|
||||||
logging.getLogger(__name__).warning(
|
logging.getLogger(__name__).warning(
|
||||||
"QQ_HOME_CHANNEL is deprecated; rename to QQBOT_HOME_CHANNEL "
|
"QQ_HOME_CHANNEL is deprecated; rename to QQBOT_HOME_CHANNEL "
|
||||||
"in your .env for consistency with the platform key."
|
"in your .env for consistency with the platform key."
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,6 @@ class ResponseStore:
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
import time
|
|
||||||
self._conn.execute(
|
self._conn.execute(
|
||||||
"UPDATE responses SET accessed_at = ? WHERE response_id = ?",
|
"UPDATE responses SET accessed_at = ? WHERE response_id = ?",
|
||||||
(time.time(), response_id),
|
(time.time(), response_id),
|
||||||
|
|
@ -333,7 +332,6 @@ class ResponseStore:
|
||||||
|
|
||||||
def put(self, response_id: str, data: Dict[str, Any]) -> None:
|
def put(self, response_id: str, data: Dict[str, Any]) -> None:
|
||||||
"""Store a response, evicting the oldest if at capacity."""
|
"""Store a response, evicting the oldest if at capacity."""
|
||||||
import time
|
|
||||||
self._conn.execute(
|
self._conn.execute(
|
||||||
"INSERT OR REPLACE INTO responses (response_id, data, accessed_at) VALUES (?, ?, ?)",
|
"INSERT OR REPLACE INTO responses (response_id, data, accessed_at) VALUES (?, ?, ?)",
|
||||||
(response_id, json.dumps(data, default=str), time.time()),
|
(response_id, json.dumps(data, default=str), time.time()),
|
||||||
|
|
@ -474,8 +472,7 @@ class _IdempotencyCache:
|
||||||
self._max = max_items
|
self._max = max_items
|
||||||
|
|
||||||
def _purge(self):
|
def _purge(self):
|
||||||
import time as _t
|
now = time.time()
|
||||||
now = _t.time()
|
|
||||||
expired = [k for k, v in self._store.items() if now - v["ts"] > self._ttl]
|
expired = [k for k, v in self._store.items() if now - v["ts"] > self._ttl]
|
||||||
for k in expired:
|
for k in expired:
|
||||||
self._store.pop(k, None)
|
self._store.pop(k, None)
|
||||||
|
|
@ -537,6 +534,30 @@ def _derive_chat_session_id(
|
||||||
return f"api-{digest}"
|
return f"api-{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
_CRON_AVAILABLE = False
|
||||||
|
try:
|
||||||
|
from cron.jobs import (
|
||||||
|
list_jobs as _cron_list,
|
||||||
|
get_job as _cron_get,
|
||||||
|
create_job as _cron_create,
|
||||||
|
update_job as _cron_update,
|
||||||
|
remove_job as _cron_remove,
|
||||||
|
pause_job as _cron_pause,
|
||||||
|
resume_job as _cron_resume,
|
||||||
|
trigger_job as _cron_trigger,
|
||||||
|
)
|
||||||
|
_CRON_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_cron_list = None
|
||||||
|
_cron_get = None
|
||||||
|
_cron_create = None
|
||||||
|
_cron_update = None
|
||||||
|
_cron_remove = None
|
||||||
|
_cron_pause = None
|
||||||
|
_cron_resume = None
|
||||||
|
_cron_trigger = None
|
||||||
|
|
||||||
|
|
||||||
class APIServerAdapter(BasePlatformAdapter):
|
class APIServerAdapter(BasePlatformAdapter):
|
||||||
"""
|
"""
|
||||||
OpenAI-compatible HTTP API server adapter.
|
OpenAI-compatible HTTP API server adapter.
|
||||||
|
|
@ -1866,44 +1887,16 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
# Cron jobs API
|
# Cron jobs API
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
# Check cron module availability once (not per-request)
|
|
||||||
_CRON_AVAILABLE = False
|
|
||||||
try:
|
|
||||||
from cron.jobs import (
|
|
||||||
list_jobs as _cron_list,
|
|
||||||
get_job as _cron_get,
|
|
||||||
create_job as _cron_create,
|
|
||||||
update_job as _cron_update,
|
|
||||||
remove_job as _cron_remove,
|
|
||||||
pause_job as _cron_pause,
|
|
||||||
resume_job as _cron_resume,
|
|
||||||
trigger_job as _cron_trigger,
|
|
||||||
)
|
|
||||||
# Wrap as staticmethod to prevent descriptor binding — these are plain
|
|
||||||
# module functions, not instance methods. Without this, self._cron_*()
|
|
||||||
# injects ``self`` as the first positional argument and every call
|
|
||||||
# raises TypeError.
|
|
||||||
_cron_list = staticmethod(_cron_list)
|
|
||||||
_cron_get = staticmethod(_cron_get)
|
|
||||||
_cron_create = staticmethod(_cron_create)
|
|
||||||
_cron_update = staticmethod(_cron_update)
|
|
||||||
_cron_remove = staticmethod(_cron_remove)
|
|
||||||
_cron_pause = staticmethod(_cron_pause)
|
|
||||||
_cron_resume = staticmethod(_cron_resume)
|
|
||||||
_cron_trigger = staticmethod(_cron_trigger)
|
|
||||||
_CRON_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
_JOB_ID_RE = __import__("re").compile(r"[a-f0-9]{12}")
|
_JOB_ID_RE = __import__("re").compile(r"[a-f0-9]{12}")
|
||||||
# Allowed fields for update — prevents clients injecting arbitrary keys
|
# Allowed fields for update — prevents clients injecting arbitrary keys
|
||||||
_UPDATE_ALLOWED_FIELDS = {"name", "schedule", "prompt", "deliver", "skills", "skill", "repeat", "enabled"}
|
_UPDATE_ALLOWED_FIELDS = {"name", "schedule", "prompt", "deliver", "skills", "skill", "repeat", "enabled"}
|
||||||
_MAX_NAME_LENGTH = 200
|
_MAX_NAME_LENGTH = 200
|
||||||
_MAX_PROMPT_LENGTH = 5000
|
_MAX_PROMPT_LENGTH = 5000
|
||||||
|
|
||||||
def _check_jobs_available(self) -> Optional["web.Response"]:
|
@staticmethod
|
||||||
|
def _check_jobs_available() -> Optional["web.Response"]:
|
||||||
"""Return error response if cron module isn't available."""
|
"""Return error response if cron module isn't available."""
|
||||||
if not self._CRON_AVAILABLE:
|
if not _CRON_AVAILABLE:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"error": "Cron module not available"}, status=501,
|
{"error": "Cron module not available"}, status=501,
|
||||||
)
|
)
|
||||||
|
|
@ -1928,7 +1921,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
return cron_err
|
return cron_err
|
||||||
try:
|
try:
|
||||||
include_disabled = request.query.get("include_disabled", "").lower() in ("true", "1")
|
include_disabled = request.query.get("include_disabled", "").lower() in ("true", "1")
|
||||||
jobs = self._cron_list(include_disabled=include_disabled)
|
jobs = _cron_list(include_disabled=include_disabled)
|
||||||
return web.json_response({"jobs": jobs})
|
return web.json_response({"jobs": jobs})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
@ -1976,7 +1969,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if repeat is not None:
|
if repeat is not None:
|
||||||
kwargs["repeat"] = repeat
|
kwargs["repeat"] = repeat
|
||||||
|
|
||||||
job = self._cron_create(**kwargs)
|
job = _cron_create(**kwargs)
|
||||||
return web.json_response({"job": job})
|
return web.json_response({"job": job})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
@ -1993,7 +1986,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if id_err:
|
if id_err:
|
||||||
return id_err
|
return id_err
|
||||||
try:
|
try:
|
||||||
job = self._cron_get(job_id)
|
job = _cron_get(job_id)
|
||||||
if not job:
|
if not job:
|
||||||
return web.json_response({"error": "Job not found"}, status=404)
|
return web.json_response({"error": "Job not found"}, status=404)
|
||||||
return web.json_response({"job": job})
|
return web.json_response({"job": job})
|
||||||
|
|
@ -2026,7 +2019,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"error": f"Prompt must be ≤ {self._MAX_PROMPT_LENGTH} characters"}, status=400,
|
{"error": f"Prompt must be ≤ {self._MAX_PROMPT_LENGTH} characters"}, status=400,
|
||||||
)
|
)
|
||||||
job = self._cron_update(job_id, sanitized)
|
job = _cron_update(job_id, sanitized)
|
||||||
if not job:
|
if not job:
|
||||||
return web.json_response({"error": "Job not found"}, status=404)
|
return web.json_response({"error": "Job not found"}, status=404)
|
||||||
return web.json_response({"job": job})
|
return web.json_response({"job": job})
|
||||||
|
|
@ -2045,7 +2038,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if id_err:
|
if id_err:
|
||||||
return id_err
|
return id_err
|
||||||
try:
|
try:
|
||||||
success = self._cron_remove(job_id)
|
success = _cron_remove(job_id)
|
||||||
if not success:
|
if not success:
|
||||||
return web.json_response({"error": "Job not found"}, status=404)
|
return web.json_response({"error": "Job not found"}, status=404)
|
||||||
return web.json_response({"ok": True})
|
return web.json_response({"ok": True})
|
||||||
|
|
@ -2064,7 +2057,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if id_err:
|
if id_err:
|
||||||
return id_err
|
return id_err
|
||||||
try:
|
try:
|
||||||
job = self._cron_pause(job_id)
|
job = _cron_pause(job_id)
|
||||||
if not job:
|
if not job:
|
||||||
return web.json_response({"error": "Job not found"}, status=404)
|
return web.json_response({"error": "Job not found"}, status=404)
|
||||||
return web.json_response({"job": job})
|
return web.json_response({"job": job})
|
||||||
|
|
@ -2083,7 +2076,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if id_err:
|
if id_err:
|
||||||
return id_err
|
return id_err
|
||||||
try:
|
try:
|
||||||
job = self._cron_resume(job_id)
|
job = _cron_resume(job_id)
|
||||||
if not job:
|
if not job:
|
||||||
return web.json_response({"error": "Job not found"}, status=404)
|
return web.json_response({"error": "Job not found"}, status=404)
|
||||||
return web.json_response({"job": job})
|
return web.json_response({"job": job})
|
||||||
|
|
@ -2102,7 +2095,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
if id_err:
|
if id_err:
|
||||||
return id_err
|
return id_err
|
||||||
try:
|
try:
|
||||||
job = self._cron_trigger(job_id)
|
job = _cron_trigger(job_id)
|
||||||
if not job:
|
if not job:
|
||||||
return web.json_response({"error": "Job not found"}, status=404)
|
return web.json_response({"error": "Job not found"}, status=404)
|
||||||
return web.json_response({"job": job})
|
return web.json_response({"job": job})
|
||||||
|
|
|
||||||
|
|
@ -391,12 +391,9 @@ async def cache_image_from_url(url: str, ext: str = ".jpg", retries: int = 2) ->
|
||||||
if not is_safe_url(url):
|
if not is_safe_url(url):
|
||||||
raise ValueError(f"Blocked unsafe URL (SSRF protection): {safe_url_for_log(url)}")
|
raise ValueError(f"Blocked unsafe URL (SSRF protection): {safe_url_for_log(url)}")
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import httpx
|
import httpx
|
||||||
import logging as _logging
|
_log = logging.getLogger(__name__)
|
||||||
_log = _logging.getLogger(__name__)
|
|
||||||
|
|
||||||
last_exc = None
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
timeout=30.0,
|
timeout=30.0,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
|
|
@ -414,7 +411,6 @@ async def cache_image_from_url(url: str, ext: str = ".jpg", retries: int = 2) ->
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return cache_image_from_bytes(response.content, ext)
|
return cache_image_from_bytes(response.content, ext)
|
||||||
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
||||||
last_exc = exc
|
|
||||||
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
||||||
raise
|
raise
|
||||||
if attempt < retries:
|
if attempt < retries:
|
||||||
|
|
@ -430,7 +426,6 @@ async def cache_image_from_url(url: str, ext: str = ".jpg", retries: int = 2) ->
|
||||||
await asyncio.sleep(wait)
|
await asyncio.sleep(wait)
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
raise last_exc
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_image_cache(max_age_hours: int = 24) -> int:
|
def cleanup_image_cache(max_age_hours: int = 24) -> int:
|
||||||
|
|
@ -510,12 +505,9 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg", retries: int = 2) ->
|
||||||
if not is_safe_url(url):
|
if not is_safe_url(url):
|
||||||
raise ValueError(f"Blocked unsafe URL (SSRF protection): {safe_url_for_log(url)}")
|
raise ValueError(f"Blocked unsafe URL (SSRF protection): {safe_url_for_log(url)}")
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import httpx
|
import httpx
|
||||||
import logging as _logging
|
_log = logging.getLogger(__name__)
|
||||||
_log = _logging.getLogger(__name__)
|
|
||||||
|
|
||||||
last_exc = None
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
timeout=30.0,
|
timeout=30.0,
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
|
|
@ -533,7 +525,6 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg", retries: int = 2) ->
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return cache_audio_from_bytes(response.content, ext)
|
return cache_audio_from_bytes(response.content, ext)
|
||||||
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
||||||
last_exc = exc
|
|
||||||
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
||||||
raise
|
raise
|
||||||
if attempt < retries:
|
if attempt < retries:
|
||||||
|
|
@ -549,7 +540,6 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg", retries: int = 2) ->
|
||||||
await asyncio.sleep(wait)
|
await asyncio.sleep(wait)
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
raise last_exc
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -1787,8 +1777,6 @@ class BasePlatformAdapter(ABC):
|
||||||
HERMES_HUMAN_DELAY_MIN_MS: minimum delay in ms (default 800, custom mode)
|
HERMES_HUMAN_DELAY_MIN_MS: minimum delay in ms (default 800, custom mode)
|
||||||
HERMES_HUMAN_DELAY_MAX_MS: maximum delay in ms (default 2500, custom mode)
|
HERMES_HUMAN_DELAY_MAX_MS: maximum delay in ms (default 2500, custom mode)
|
||||||
"""
|
"""
|
||||||
import random
|
|
||||||
|
|
||||||
mode = os.getenv("HERMES_HUMAN_DELAY_MODE", "off").lower()
|
mode = os.getenv("HERMES_HUMAN_DELAY_MODE", "off").lower()
|
||||||
if mode == "off":
|
if mode == "off":
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
|
||||||
|
|
@ -541,7 +541,6 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
# ctypes.util.find_library fails on macOS with Homebrew-installed libs,
|
# ctypes.util.find_library fails on macOS with Homebrew-installed libs,
|
||||||
# so fall back to known Homebrew paths if needed.
|
# so fall back to known Homebrew paths if needed.
|
||||||
if not opus_path:
|
if not opus_path:
|
||||||
import sys
|
|
||||||
_homebrew_paths = (
|
_homebrew_paths = (
|
||||||
"/opt/homebrew/lib/libopus.dylib", # Apple Silicon
|
"/opt/homebrew/lib/libopus.dylib", # Apple Silicon
|
||||||
"/usr/local/lib/libopus.dylib", # Intel Mac
|
"/usr/local/lib/libopus.dylib", # Intel Mac
|
||||||
|
|
@ -1422,8 +1421,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||||
speaking_user_ids: set = set()
|
speaking_user_ids: set = set()
|
||||||
receiver = self._voice_receivers.get(guild_id)
|
receiver = self._voice_receivers.get(guild_id)
|
||||||
if receiver:
|
if receiver:
|
||||||
import time as _time
|
now = time.monotonic()
|
||||||
now = _time.monotonic()
|
|
||||||
with receiver._lock:
|
with receiver._lock:
|
||||||
for ssrc, last_t in receiver._last_packet_time.items():
|
for ssrc, last_t in receiver._last_packet_time.items():
|
||||||
# Consider "speaking" if audio received within last 2 seconds
|
# Consider "speaking" if audio received within last 2 seconds
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,6 @@ class MattermostAdapter(BasePlatformAdapter):
|
||||||
logger.warning("Mattermost: blocked unsafe URL (SSRF protection)")
|
logger.warning("Mattermost: blocked unsafe URL (SSRF protection)")
|
||||||
return await self.send(chat_id, f"{caption or ''}\n{url}".strip(), reply_to)
|
return await self.send(chat_id, f"{caption or ''}\n{url}".strip(), reply_to)
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
last_exc = None
|
last_exc = None
|
||||||
|
|
|
||||||
|
|
@ -1086,11 +1086,8 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
return MessageType.VIDEO
|
return MessageType.VIDEO
|
||||||
if "image" in first_type or "photo" in first_type:
|
if "image" in first_type or "photo" in first_type:
|
||||||
return MessageType.PHOTO
|
return MessageType.PHOTO
|
||||||
# Unknown content type with an attachment — don't assume PHOTO
|
|
||||||
# to prevent non-image files from being sent to vision analysis.
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[%s] Unknown media content_type '%s', defaulting to TEXT",
|
"Unknown media content_type '%s', defaulting to TEXT",
|
||||||
self._log_tag,
|
|
||||||
first_type,
|
first_type,
|
||||||
)
|
)
|
||||||
return MessageType.TEXT
|
return MessageType.TEXT
|
||||||
|
|
@ -1826,14 +1823,12 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
body["file_name"] = file_name
|
body["file_name"] = file_name
|
||||||
|
|
||||||
# Retry transient upload failures
|
# Retry transient upload failures
|
||||||
last_exc = None
|
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
return await self._api_request(
|
return await self._api_request(
|
||||||
"POST", path, body, timeout=FILE_UPLOAD_TIMEOUT
|
"POST", path, body, timeout=FILE_UPLOAD_TIMEOUT
|
||||||
)
|
)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
last_exc = exc
|
|
||||||
err_msg = str(exc)
|
err_msg = str(exc)
|
||||||
if any(
|
if any(
|
||||||
kw in err_msg
|
kw in err_msg
|
||||||
|
|
@ -1842,8 +1837,8 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
raise
|
raise
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
await asyncio.sleep(1.5 * (attempt + 1))
|
await asyncio.sleep(1.5 * (attempt + 1))
|
||||||
|
else:
|
||||||
raise last_exc # type: ignore[misc]
|
raise
|
||||||
|
|
||||||
# Maximum time (seconds) to wait for reconnection before giving up on send.
|
# Maximum time (seconds) to wait for reconnection before giving up on send.
|
||||||
_RECONNECT_WAIT_SECONDS = 15.0
|
_RECONNECT_WAIT_SECONDS = 15.0
|
||||||
|
|
|
||||||
|
|
@ -1600,11 +1600,9 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
async def _download_slack_file(self, url: str, ext: str, audio: bool = False, team_id: str = "") -> str:
|
async def _download_slack_file(self, url: str, ext: str, audio: bool = False, team_id: str = "") -> str:
|
||||||
"""Download a Slack file using the bot token for auth, with retry."""
|
"""Download a Slack file using the bot token for auth, with retry."""
|
||||||
import asyncio
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
bot_token = self._team_clients[team_id].token if team_id and team_id in self._team_clients else self.config.token
|
bot_token = self._team_clients[team_id].token if team_id and team_id in self._team_clients else self.config.token
|
||||||
last_exc = None
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
|
|
@ -1634,7 +1632,6 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
from gateway.platforms.base import cache_image_from_bytes
|
from gateway.platforms.base import cache_image_from_bytes
|
||||||
return cache_image_from_bytes(response.content, ext)
|
return cache_image_from_bytes(response.content, ext)
|
||||||
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
||||||
last_exc = exc
|
|
||||||
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
||||||
raise
|
raise
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
|
|
@ -1643,15 +1640,12 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
await asyncio.sleep(1.5 * (attempt + 1))
|
await asyncio.sleep(1.5 * (attempt + 1))
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
raise last_exc
|
|
||||||
|
|
||||||
async def _download_slack_file_bytes(self, url: str, team_id: str = "") -> bytes:
|
async def _download_slack_file_bytes(self, url: str, team_id: str = "") -> bytes:
|
||||||
"""Download a Slack file and return raw bytes, with retry."""
|
"""Download a Slack file and return raw bytes, with retry."""
|
||||||
import asyncio
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
bot_token = self._team_clients[team_id].token if team_id and team_id in self._team_clients else self.config.token
|
bot_token = self._team_clients[team_id].token if team_id and team_id in self._team_clients else self.config.token
|
||||||
last_exc = None
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
|
|
@ -1663,7 +1657,6 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.content
|
return response.content
|
||||||
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
|
||||||
last_exc = exc
|
|
||||||
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
|
||||||
raise
|
raise
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
|
|
@ -1672,7 +1665,6 @@ class SlackAdapter(BasePlatformAdapter):
|
||||||
await asyncio.sleep(1.5 * (attempt + 1))
|
await asyncio.sleep(1.5 * (attempt + 1))
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
raise last_exc
|
|
||||||
|
|
||||||
# ── Channel mention gating ─────────────────────────────────────────────
|
# ── Channel mention gating ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1713,7 +1713,6 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
return SendResult(success=False, error="Not connected")
|
return SendResult(success=False, error="Not connected")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import os
|
|
||||||
if not os.path.exists(audio_path):
|
if not os.path.exists(audio_path):
|
||||||
return SendResult(success=False, error=self._missing_media_path_error("Audio", audio_path))
|
return SendResult(success=False, error=self._missing_media_path_error("Audio", audio_path))
|
||||||
|
|
||||||
|
|
@ -1762,7 +1761,6 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
return SendResult(success=False, error="Not connected")
|
return SendResult(success=False, error="Not connected")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import os
|
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
return SendResult(success=False, error=self._missing_media_path_error("Image", image_path))
|
return SendResult(success=False, error=self._missing_media_path_error("Image", image_path))
|
||||||
|
|
||||||
|
|
@ -2823,13 +2821,11 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||||
logger.info("[Telegram] Analyzing sticker at %s", cached_path)
|
logger.info("[Telegram] Analyzing sticker at %s", cached_path)
|
||||||
|
|
||||||
from tools.vision_tools import vision_analyze_tool
|
from tools.vision_tools import vision_analyze_tool
|
||||||
import json as _json
|
|
||||||
|
|
||||||
result_json = await vision_analyze_tool(
|
result_json = await vision_analyze_tool(
|
||||||
image_url=cached_path,
|
image_url=cached_path,
|
||||||
user_prompt=STICKER_VISION_PROMPT,
|
user_prompt=STICKER_VISION_PROMPT,
|
||||||
)
|
)
|
||||||
result = _json.loads(result_json)
|
result = json.loads(result_json)
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
description = result.get("analysis", "a sticker")
|
description = result.get("analysis", "a sticker")
|
||||||
|
|
|
||||||
|
|
@ -624,13 +624,16 @@ class WeComAdapter(BasePlatformAdapter):
|
||||||
msgtype = str(body.get("msgtype") or "").lower()
|
msgtype = str(body.get("msgtype") or "").lower()
|
||||||
|
|
||||||
if msgtype == "mixed":
|
if msgtype == "mixed":
|
||||||
mixed = body.get("mixed") if isinstance(body.get("mixed"), dict) else {}
|
_raw_mixed = body.get("mixed")
|
||||||
items = mixed.get("msg_item") if isinstance(mixed.get("msg_item"), list) else []
|
mixed = _raw_mixed if isinstance(_raw_mixed, dict) else {}
|
||||||
|
_raw_items = mixed.get("msg_item")
|
||||||
|
items = _raw_items if isinstance(_raw_items, list) else []
|
||||||
for item in items:
|
for item in items:
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
if str(item.get("msgtype") or "").lower() == "text":
|
if str(item.get("msgtype") or "").lower() == "text":
|
||||||
text_block = item.get("text") if isinstance(item.get("text"), dict) else {}
|
_raw_text = item.get("text")
|
||||||
|
text_block = _raw_text if isinstance(_raw_text, dict) else {}
|
||||||
content = str(text_block.get("content") or "").strip()
|
content = str(text_block.get("content") or "").strip()
|
||||||
if content:
|
if content:
|
||||||
text_parts.append(content)
|
text_parts.append(content)
|
||||||
|
|
@ -672,8 +675,10 @@ class WeComAdapter(BasePlatformAdapter):
|
||||||
msgtype = str(body.get("msgtype") or "").lower()
|
msgtype = str(body.get("msgtype") or "").lower()
|
||||||
|
|
||||||
if msgtype == "mixed":
|
if msgtype == "mixed":
|
||||||
mixed = body.get("mixed") if isinstance(body.get("mixed"), dict) else {}
|
_raw_mixed = body.get("mixed")
|
||||||
items = mixed.get("msg_item") if isinstance(mixed.get("msg_item"), list) else []
|
mixed = _raw_mixed if isinstance(_raw_mixed, dict) else {}
|
||||||
|
_raw_items = mixed.get("msg_item")
|
||||||
|
items = _raw_items if isinstance(_raw_items, list) else []
|
||||||
for item in items:
|
for item in items:
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -1266,7 +1266,6 @@ class GatewayRunner:
|
||||||
the prefill_messages_file key in ~/.hermes/config.yaml.
|
the prefill_messages_file key in ~/.hermes/config.yaml.
|
||||||
Relative paths are resolved from ~/.hermes/.
|
Relative paths are resolved from ~/.hermes/.
|
||||||
"""
|
"""
|
||||||
import json as _json
|
|
||||||
file_path = os.getenv("HERMES_PREFILL_MESSAGES_FILE", "")
|
file_path = os.getenv("HERMES_PREFILL_MESSAGES_FILE", "")
|
||||||
if not file_path:
|
if not file_path:
|
||||||
try:
|
try:
|
||||||
|
|
@ -1288,7 +1287,7 @@ class GatewayRunner:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
data = _json.load(f)
|
data = json.load(f)
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
logger.warning("Prefill messages file must contain a JSON array: %s", path)
|
logger.warning("Prefill messages file must contain a JSON array: %s", path)
|
||||||
return []
|
return []
|
||||||
|
|
@ -3675,9 +3674,8 @@ class GatewayRunner:
|
||||||
plugin_handler = get_plugin_command_handler(command.replace("_", "-"))
|
plugin_handler = get_plugin_command_handler(command.replace("_", "-"))
|
||||||
if plugin_handler:
|
if plugin_handler:
|
||||||
user_args = event.get_command_args().strip()
|
user_args = event.get_command_args().strip()
|
||||||
import asyncio as _aio
|
|
||||||
result = plugin_handler(user_args)
|
result = plugin_handler(user_args)
|
||||||
if _aio.iscoroutine(result):
|
if asyncio.iscoroutine(result):
|
||||||
result = await result
|
result = await result
|
||||||
return str(result) if result else None
|
return str(result) if result else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -3871,13 +3869,10 @@ class GatewayRunner:
|
||||||
if not mtype.startswith(("application/", "text/")):
|
if not mtype.startswith(("application/", "text/")):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
import os as _os
|
basename = os.path.basename(path)
|
||||||
import re as _re
|
|
||||||
|
|
||||||
basename = _os.path.basename(path)
|
|
||||||
parts = basename.split("_", 2)
|
parts = basename.split("_", 2)
|
||||||
display_name = parts[2] if len(parts) >= 3 else basename
|
display_name = parts[2] if len(parts) >= 3 else basename
|
||||||
display_name = _re.sub(r'[^\w.\- ]', '_', display_name)
|
display_name = re.sub(r'[^\w.\- ]', '_', display_name)
|
||||||
|
|
||||||
if mtype.startswith("text/"):
|
if mtype.startswith("text/"):
|
||||||
context_note = (
|
context_note = (
|
||||||
|
|
@ -5175,7 +5170,6 @@ class GatewayRunner:
|
||||||
# Save the requester's routing info so the new gateway process can
|
# Save the requester's routing info so the new gateway process can
|
||||||
# notify them once it comes back online.
|
# notify them once it comes back online.
|
||||||
try:
|
try:
|
||||||
import json as _json
|
|
||||||
notify_data = {
|
notify_data = {
|
||||||
"platform": event.source.platform.value if event.source.platform else None,
|
"platform": event.source.platform.value if event.source.platform else None,
|
||||||
"chat_id": event.source.chat_id,
|
"chat_id": event.source.chat_id,
|
||||||
|
|
@ -5183,7 +5177,7 @@ class GatewayRunner:
|
||||||
if event.source.thread_id:
|
if event.source.thread_id:
|
||||||
notify_data["thread_id"] = event.source.thread_id
|
notify_data["thread_id"] = event.source.thread_id
|
||||||
(_hermes_home / ".restart_notify.json").write_text(
|
(_hermes_home / ".restart_notify.json").write_text(
|
||||||
_json.dumps(notify_data)
|
json.dumps(notify_data)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Failed to write restart notify file: %s", e)
|
logger.debug("Failed to write restart notify file: %s", e)
|
||||||
|
|
@ -5194,16 +5188,14 @@ class GatewayRunner:
|
||||||
# marker persists so the new gateway can still detect a delayed
|
# marker persists so the new gateway can still detect a delayed
|
||||||
# /restart redelivery from Telegram. Overwritten on every /restart.
|
# /restart redelivery from Telegram. Overwritten on every /restart.
|
||||||
try:
|
try:
|
||||||
import json as _json
|
|
||||||
import time as _time
|
|
||||||
dedup_data = {
|
dedup_data = {
|
||||||
"platform": event.source.platform.value if event.source.platform else None,
|
"platform": event.source.platform.value if event.source.platform else None,
|
||||||
"requested_at": _time.time(),
|
"requested_at": time.time(),
|
||||||
}
|
}
|
||||||
if event.platform_update_id is not None:
|
if event.platform_update_id is not None:
|
||||||
dedup_data["update_id"] = event.platform_update_id
|
dedup_data["update_id"] = event.platform_update_id
|
||||||
(_hermes_home / ".restart_last_processed.json").write_text(
|
(_hermes_home / ".restart_last_processed.json").write_text(
|
||||||
_json.dumps(dedup_data)
|
json.dumps(dedup_data)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("Failed to write restart dedup marker: %s", e)
|
logger.debug("Failed to write restart dedup marker: %s", e)
|
||||||
|
|
@ -5251,12 +5243,10 @@ class GatewayRunner:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import json as _json
|
|
||||||
import time as _time
|
|
||||||
marker_path = _hermes_home / ".restart_last_processed.json"
|
marker_path = _hermes_home / ".restart_last_processed.json"
|
||||||
if not marker_path.exists():
|
if not marker_path.exists():
|
||||||
return False
|
return False
|
||||||
data = _json.loads(marker_path.read_text())
|
data = json.loads(marker_path.read_text())
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -5270,7 +5260,7 @@ class GatewayRunner:
|
||||||
# swallow a fresh /restart from the user.
|
# swallow a fresh /restart from the user.
|
||||||
requested_at = data.get("requested_at")
|
requested_at = data.get("requested_at")
|
||||||
if isinstance(requested_at, (int, float)):
|
if isinstance(requested_at, (int, float)):
|
||||||
if _time.time() - requested_at > 300:
|
if time.time() - requested_at > 300:
|
||||||
return False
|
return False
|
||||||
return event.platform_update_id <= recorded_uid
|
return event.platform_update_id <= recorded_uid
|
||||||
|
|
||||||
|
|
@ -7352,13 +7342,10 @@ class GatewayRunner:
|
||||||
|
|
||||||
async def _handle_insights_command(self, event: MessageEvent) -> str:
|
async def _handle_insights_command(self, event: MessageEvent) -> str:
|
||||||
"""Handle /insights command -- show usage insights and analytics."""
|
"""Handle /insights command -- show usage insights and analytics."""
|
||||||
import asyncio as _asyncio
|
|
||||||
|
|
||||||
args = event.get_command_args().strip()
|
args = event.get_command_args().strip()
|
||||||
|
|
||||||
# Normalize Unicode dashes (Telegram/iOS auto-converts -- to em/en dash)
|
# Normalize Unicode dashes (Telegram/iOS auto-converts -- to em/en dash)
|
||||||
import re as _re
|
args = re.sub(r'[\u2012\u2013\u2014\u2015](days|source)', r'--\1', args)
|
||||||
args = _re.sub(r'[\u2012\u2013\u2014\u2015](days|source)', r'--\1', args)
|
|
||||||
|
|
||||||
days = 30
|
days = 30
|
||||||
source = None
|
source = None
|
||||||
|
|
@ -7387,7 +7374,7 @@ class GatewayRunner:
|
||||||
from hermes_state import SessionDB
|
from hermes_state import SessionDB
|
||||||
from agent.insights import InsightsEngine
|
from agent.insights import InsightsEngine
|
||||||
|
|
||||||
loop = _asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
def _run_insights():
|
def _run_insights():
|
||||||
db = SessionDB()
|
db = SessionDB()
|
||||||
|
|
@ -7745,9 +7732,6 @@ class GatewayRunner:
|
||||||
the messenger. The user's next message is intercepted by
|
the messenger. The user's next message is intercepted by
|
||||||
``_handle_message`` and written to ``.update_response``.
|
``_handle_message`` and written to ``.update_response``.
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import re as _re
|
|
||||||
|
|
||||||
pending_path = _hermes_home / ".update_pending.json"
|
pending_path = _hermes_home / ".update_pending.json"
|
||||||
claimed_path = _hermes_home / ".update_pending.claimed.json"
|
claimed_path = _hermes_home / ".update_pending.claimed.json"
|
||||||
output_path = _hermes_home / ".update_output.txt"
|
output_path = _hermes_home / ".update_output.txt"
|
||||||
|
|
@ -7792,7 +7776,7 @@ class GatewayRunner:
|
||||||
return
|
return
|
||||||
|
|
||||||
def _strip_ansi(text: str) -> str:
|
def _strip_ansi(text: str) -> str:
|
||||||
return _re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', text)
|
return re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', text)
|
||||||
|
|
||||||
bytes_sent = 0
|
bytes_sent = 0
|
||||||
last_stream_time = loop.time()
|
last_stream_time = loop.time()
|
||||||
|
|
@ -7940,9 +7924,6 @@ class GatewayRunner:
|
||||||
cannot resolve the adapter (e.g. after a gateway restart where the
|
cannot resolve the adapter (e.g. after a gateway restart where the
|
||||||
platform hasn't reconnected yet).
|
platform hasn't reconnected yet).
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import re as _re
|
|
||||||
|
|
||||||
pending_path = _hermes_home / ".update_pending.json"
|
pending_path = _hermes_home / ".update_pending.json"
|
||||||
claimed_path = _hermes_home / ".update_pending.claimed.json"
|
claimed_path = _hermes_home / ".update_pending.claimed.json"
|
||||||
output_path = _hermes_home / ".update_output.txt"
|
output_path = _hermes_home / ".update_output.txt"
|
||||||
|
|
@ -7988,7 +7969,7 @@ class GatewayRunner:
|
||||||
|
|
||||||
if adapter and chat_id:
|
if adapter and chat_id:
|
||||||
# Strip ANSI escape codes for clean display
|
# Strip ANSI escape codes for clean display
|
||||||
output = _re.sub(r'\x1b\[[0-9;]*m', '', output).strip()
|
output = re.sub(r'\x1b\[[0-9;]*m', '', output).strip()
|
||||||
if output:
|
if output:
|
||||||
if len(output) > 3500:
|
if len(output) > 3500:
|
||||||
output = "…" + output[-3500:]
|
output = "…" + output[-3500:]
|
||||||
|
|
@ -8021,14 +8002,12 @@ class GatewayRunner:
|
||||||
|
|
||||||
async def _send_restart_notification(self) -> None:
|
async def _send_restart_notification(self) -> None:
|
||||||
"""Notify the chat that initiated /restart that the gateway is back."""
|
"""Notify the chat that initiated /restart that the gateway is back."""
|
||||||
import json as _json
|
|
||||||
|
|
||||||
notify_path = _hermes_home / ".restart_notify.json"
|
notify_path = _hermes_home / ".restart_notify.json"
|
||||||
if not notify_path.exists():
|
if not notify_path.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = _json.loads(notify_path.read_text())
|
data = json.loads(notify_path.read_text())
|
||||||
platform_str = data.get("platform")
|
platform_str = data.get("platform")
|
||||||
chat_id = data.get("chat_id")
|
chat_id = data.get("chat_id")
|
||||||
thread_id = data.get("thread_id")
|
thread_id = data.get("thread_id")
|
||||||
|
|
@ -8114,7 +8093,6 @@ class GatewayRunner:
|
||||||
The enriched message string with vision descriptions prepended.
|
The enriched message string with vision descriptions prepended.
|
||||||
"""
|
"""
|
||||||
from tools.vision_tools import vision_analyze_tool
|
from tools.vision_tools import vision_analyze_tool
|
||||||
import json as _json
|
|
||||||
|
|
||||||
analysis_prompt = (
|
analysis_prompt = (
|
||||||
"Describe everything visible in this image in thorough detail. "
|
"Describe everything visible in this image in thorough detail. "
|
||||||
|
|
@ -8130,7 +8108,7 @@ class GatewayRunner:
|
||||||
image_url=path,
|
image_url=path,
|
||||||
user_prompt=analysis_prompt,
|
user_prompt=analysis_prompt,
|
||||||
)
|
)
|
||||||
result = _json.loads(result_json)
|
result = json.loads(result_json)
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
description = result.get("analysis", "")
|
description = result.get("analysis", "")
|
||||||
enriched_parts.append(
|
enriched_parts.append(
|
||||||
|
|
@ -8189,7 +8167,6 @@ class GatewayRunner:
|
||||||
return disabled_note
|
return disabled_note
|
||||||
|
|
||||||
from tools.transcription_tools import transcribe_audio
|
from tools.transcription_tools import transcribe_audio
|
||||||
import asyncio
|
|
||||||
|
|
||||||
enriched_parts = []
|
enriched_parts = []
|
||||||
for path in audio_paths:
|
for path in audio_paths:
|
||||||
|
|
@ -9236,8 +9213,7 @@ class GatewayRunner:
|
||||||
if args:
|
if args:
|
||||||
from agent.display import get_tool_preview_max_len
|
from agent.display import get_tool_preview_max_len
|
||||||
_pl = get_tool_preview_max_len()
|
_pl = get_tool_preview_max_len()
|
||||||
import json as _json
|
args_str = json.dumps(args, ensure_ascii=False, default=str)
|
||||||
args_str = _json.dumps(args, ensure_ascii=False, default=str)
|
|
||||||
# When tool_preview_length is 0 (default), don't truncate
|
# When tool_preview_length is 0 (default), don't truncate
|
||||||
# in verbose mode — the user explicitly asked for full
|
# in verbose mode — the user explicitly asked for full
|
||||||
# detail. Platform message-length limits handle the rest.
|
# detail. Platform message-length limits handle the rest.
|
||||||
|
|
@ -10752,7 +10728,6 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool =
|
||||||
# The PID file is scoped to HERMES_HOME, so future multi-profile
|
# The PID file is scoped to HERMES_HOME, so future multi-profile
|
||||||
# setups (each profile using a distinct HERMES_HOME) will naturally
|
# setups (each profile using a distinct HERMES_HOME) will naturally
|
||||||
# allow concurrent instances without tripping this guard.
|
# allow concurrent instances without tripping this guard.
|
||||||
import time as _time
|
|
||||||
from gateway.status import get_running_pid, remove_pid_file, terminate_pid
|
from gateway.status import get_running_pid, remove_pid_file, terminate_pid
|
||||||
existing_pid = get_running_pid()
|
existing_pid = get_running_pid()
|
||||||
if existing_pid is not None and existing_pid != os.getpid():
|
if existing_pid is not None and existing_pid != os.getpid():
|
||||||
|
|
@ -10792,7 +10767,7 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool =
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
try:
|
try:
|
||||||
os.kill(existing_pid, 0)
|
os.kill(existing_pid, 0)
|
||||||
_time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
except (ProcessLookupError, PermissionError):
|
except (ProcessLookupError, PermissionError):
|
||||||
break # Process is gone
|
break # Process is gone
|
||||||
else:
|
else:
|
||||||
|
|
@ -10803,7 +10778,7 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool =
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
terminate_pid(existing_pid, force=True)
|
terminate_pid(existing_pid, force=True)
|
||||||
_time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
except (ProcessLookupError, PermissionError, OSError):
|
except (ProcessLookupError, PermissionError, OSError):
|
||||||
pass
|
pass
|
||||||
remove_pid_file()
|
remove_pid_file()
|
||||||
|
|
|
||||||
|
|
@ -2249,7 +2249,6 @@ def print_config_warnings(config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
if not issues:
|
if not issues:
|
||||||
return
|
return
|
||||||
|
|
||||||
import sys
|
|
||||||
lines = ["\033[33m⚠ Config issues detected in config.yaml:\033[0m"]
|
lines = ["\033[33m⚠ Config issues detected in config.yaml:\033[0m"]
|
||||||
for ci in issues:
|
for ci in issues:
|
||||||
marker = "\033[31m✗\033[0m" if ci.severity == "error" else "\033[33m⚠\033[0m"
|
marker = "\033[31m✗\033[0m" if ci.severity == "error" else "\033[33m⚠\033[0m"
|
||||||
|
|
@ -2264,7 +2263,6 @@ def warn_deprecated_cwd_env_vars(config: Optional[Dict[str, Any]] = None) -> Non
|
||||||
These env vars are deprecated — the canonical setting is terminal.cwd
|
These env vars are deprecated — the canonical setting is terminal.cwd
|
||||||
in config.yaml. Prints a migration hint to stderr.
|
in config.yaml. Prints a migration hint to stderr.
|
||||||
"""
|
"""
|
||||||
import os, sys
|
|
||||||
messaging_cwd = os.environ.get("MESSAGING_CWD")
|
messaging_cwd = os.environ.get("MESSAGING_CWD")
|
||||||
terminal_cwd_env = os.environ.get("TERMINAL_CWD")
|
terminal_cwd_env = os.environ.get("TERMINAL_CWD")
|
||||||
|
|
||||||
|
|
@ -3273,7 +3271,6 @@ def _check_non_ascii_credential(key: str, value: str) -> str:
|
||||||
bad_chars.append(f" position {i}: {ch!r} (U+{ord(ch):04X})")
|
bad_chars.append(f" position {i}: {ch!r} (U+{ord(ch):04X})")
|
||||||
sanitized = value.encode("ascii", errors="ignore").decode("ascii")
|
sanitized = value.encode("ascii", errors="ignore").decode("ascii")
|
||||||
|
|
||||||
import sys
|
|
||||||
print(
|
print(
|
||||||
f"\n Warning: {key} contains non-ASCII characters that will break API requests.\n"
|
f"\n Warning: {key} contains non-ASCII characters that will break API requests.\n"
|
||||||
f" This usually happens when copy-pasting from a PDF, rich-text editor,\n"
|
f" This usually happens when copy-pasting from a PDF, rich-text editor,\n"
|
||||||
|
|
|
||||||
|
|
@ -994,8 +994,6 @@ def get_systemd_linger_status() -> tuple[bool | None, str]:
|
||||||
if not is_linux():
|
if not is_linux():
|
||||||
return None, "not supported on this platform"
|
return None, "not supported on this platform"
|
||||||
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
if not shutil.which("loginctl"):
|
if not shutil.which("loginctl"):
|
||||||
return None, "loginctl not found"
|
return None, "loginctl not found"
|
||||||
|
|
||||||
|
|
@ -1347,7 +1345,6 @@ def _ensure_linger_enabled() -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
import getpass
|
import getpass
|
||||||
import shutil
|
|
||||||
|
|
||||||
username = getpass.getuser()
|
username = getpass.getuser()
|
||||||
linger_file = Path(f"/var/lib/systemd/linger/{username}")
|
linger_file = Path(f"/var/lib/systemd/linger/{username}")
|
||||||
|
|
@ -1656,7 +1653,6 @@ def get_launchd_label() -> str:
|
||||||
|
|
||||||
|
|
||||||
def _launchd_domain() -> str:
|
def _launchd_domain() -> str:
|
||||||
import os
|
|
||||||
return f"gui/{os.getuid()}"
|
return f"gui/{os.getuid()}"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -618,7 +618,6 @@ def _exec_in_container(container_info: dict, cli_args: list):
|
||||||
container_info: dict with backend, container_name, exec_user, hermes_bin
|
container_info: dict with backend, container_name, exec_user, hermes_bin
|
||||||
cli_args: the original CLI arguments (everything after 'hermes')
|
cli_args: the original CLI arguments (everything after 'hermes')
|
||||||
"""
|
"""
|
||||||
import shutil
|
|
||||||
|
|
||||||
backend = container_info["backend"]
|
backend = container_info["backend"]
|
||||||
container_name = container_info["container_name"]
|
container_name = container_info["container_name"]
|
||||||
|
|
@ -1181,8 +1180,6 @@ def cmd_gateway(args):
|
||||||
def cmd_whatsapp(args):
|
def cmd_whatsapp(args):
|
||||||
"""Set up WhatsApp: choose mode, configure, install bridge, pair via QR."""
|
"""Set up WhatsApp: choose mode, configure, install bridge, pair via QR."""
|
||||||
_require_tty("whatsapp")
|
_require_tty("whatsapp")
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
from hermes_cli.config import get_env_value, save_env_value
|
from hermes_cli.config import get_env_value, save_env_value
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
@ -1425,8 +1422,6 @@ def select_provider_and_model(args=None):
|
||||||
|
|
||||||
# Read effective provider the same way the CLI does at startup:
|
# Read effective provider the same way the CLI does at startup:
|
||||||
# config.yaml model.provider > env var > auto-detect
|
# config.yaml model.provider > env var > auto-detect
|
||||||
import os
|
|
||||||
|
|
||||||
config_provider = None
|
config_provider = None
|
||||||
model_cfg = config.get("model")
|
model_cfg = config.get("model")
|
||||||
if isinstance(model_cfg, dict):
|
if isinstance(model_cfg, dict):
|
||||||
|
|
@ -2132,7 +2127,6 @@ def _model_flow_nous(config, current_model="", args=None):
|
||||||
save_env_value,
|
save_env_value,
|
||||||
)
|
)
|
||||||
from hermes_cli.nous_subscription import prompt_enable_tool_gateway
|
from hermes_cli.nous_subscription import prompt_enable_tool_gateway
|
||||||
import argparse
|
|
||||||
|
|
||||||
state = get_provider_auth_state("nous")
|
state = get_provider_auth_state("nous")
|
||||||
if not state or not state.get("access_token"):
|
if not state or not state.get("access_token"):
|
||||||
|
|
@ -2300,7 +2294,6 @@ def _model_flow_openai_codex(config, current_model=""):
|
||||||
DEFAULT_CODEX_BASE_URL,
|
DEFAULT_CODEX_BASE_URL,
|
||||||
)
|
)
|
||||||
from hermes_cli.codex_models import get_codex_model_ids
|
from hermes_cli.codex_models import get_codex_model_ids
|
||||||
import argparse
|
|
||||||
|
|
||||||
status = get_codex_auth_status()
|
status = get_codex_auth_status()
|
||||||
if not status.get("logged_in"):
|
if not status.get("logged_in"):
|
||||||
|
|
@ -4287,9 +4280,7 @@ def _clear_bytecode_cache(root: Path) -> int:
|
||||||
]
|
]
|
||||||
if os.path.basename(dirpath) == "__pycache__":
|
if os.path.basename(dirpath) == "__pycache__":
|
||||||
try:
|
try:
|
||||||
import shutil as _shutil
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
_shutil.rmtree(dirpath)
|
|
||||||
removed += 1
|
removed += 1
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
@ -4361,7 +4352,6 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
if not (web_dir / "package.json").exists():
|
if not (web_dir / "package.json").exists():
|
||||||
return True
|
return True
|
||||||
import shutil
|
|
||||||
|
|
||||||
npm = shutil.which("npm")
|
npm = shutil.which("npm")
|
||||||
if not npm:
|
if not npm:
|
||||||
|
|
@ -4398,7 +4388,6 @@ def _update_via_zip(args):
|
||||||
Used on Windows when git file I/O is broken (antivirus, NTFS filter
|
Used on Windows when git file I/O is broken (antivirus, NTFS filter
|
||||||
drivers causing 'Invalid argument' errors on file creation).
|
drivers causing 'Invalid argument' errors on file creation).
|
||||||
"""
|
"""
|
||||||
import shutil
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from urllib.request import urlretrieve
|
from urllib.request import urlretrieve
|
||||||
|
|
@ -4475,7 +4464,6 @@ def _update_via_zip(args):
|
||||||
# breaks on this machine, keep base deps and reinstall the remaining extras
|
# breaks on this machine, keep base deps and reinstall the remaining extras
|
||||||
# individually so update does not silently strip working capabilities.
|
# individually so update does not silently strip working capabilities.
|
||||||
print("→ Updating Python dependencies...")
|
print("→ Updating Python dependencies...")
|
||||||
import subprocess
|
|
||||||
|
|
||||||
uv_bin = shutil.which("uv")
|
uv_bin = shutil.which("uv")
|
||||||
if uv_bin:
|
if uv_bin:
|
||||||
|
|
@ -8078,7 +8066,6 @@ Examples:
|
||||||
return
|
return
|
||||||
line = _json.dumps(data, ensure_ascii=False) + "\n"
|
line = _json.dumps(data, ensure_ascii=False) + "\n"
|
||||||
if args.output == "-":
|
if args.output == "-":
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.stdout.write(line)
|
sys.stdout.write(line)
|
||||||
else:
|
else:
|
||||||
|
|
@ -8088,7 +8075,6 @@ Examples:
|
||||||
else:
|
else:
|
||||||
sessions = db.export_all(source=args.source)
|
sessions = db.export_all(source=args.source)
|
||||||
if args.output == "-":
|
if args.output == "-":
|
||||||
import sys
|
|
||||||
|
|
||||||
for s in sessions:
|
for s in sessions:
|
||||||
sys.stdout.write(_json.dumps(s, ensure_ascii=False) + "\n")
|
sys.stdout.write(_json.dumps(s, ensure_ascii=False) + "\n")
|
||||||
|
|
|
||||||
|
|
@ -515,8 +515,6 @@ def check_nous_free_tier() -> bool:
|
||||||
Returns False (assume paid) on any error — never blocks paying users.
|
Returns False (assume paid) on any error — never blocks paying users.
|
||||||
"""
|
"""
|
||||||
global _free_tier_cache
|
global _free_tier_cache
|
||||||
import time
|
|
||||||
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if _free_tier_cache is not None:
|
if _free_tier_cache is not None:
|
||||||
cached_result, cached_at = _free_tier_cache
|
cached_result, cached_at = _free_tier_cache
|
||||||
|
|
@ -1259,7 +1257,6 @@ def detect_provider_for_model(
|
||||||
from hermes_cli.auth import PROVIDER_REGISTRY
|
from hermes_cli.auth import PROVIDER_REGISTRY
|
||||||
pconfig = PROVIDER_REGISTRY.get(direct_match)
|
pconfig = PROVIDER_REGISTRY.get(direct_match)
|
||||||
if pconfig:
|
if pconfig:
|
||||||
import os
|
|
||||||
for env_var in pconfig.api_key_env_vars:
|
for env_var in pconfig.api_key_env_vars:
|
||||||
if os.getenv(env_var, "").strip():
|
if os.getenv(env_var, "").strip():
|
||||||
has_creds = True
|
has_creds = True
|
||||||
|
|
|
||||||
|
|
@ -849,7 +849,6 @@ def setup_model_provider(config: dict, *, quick: bool = False):
|
||||||
|
|
||||||
def _check_espeak_ng() -> bool:
|
def _check_espeak_ng() -> bool:
|
||||||
"""Check if espeak-ng is installed."""
|
"""Check if espeak-ng is installed."""
|
||||||
import shutil
|
|
||||||
return shutil.which("espeak-ng") is not None or shutil.which("espeak") is not None
|
return shutil.which("espeak-ng") is not None or shutil.which("espeak") is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1084,8 +1083,6 @@ def setup_tts(config: dict):
|
||||||
def setup_terminal_backend(config: dict):
|
def setup_terminal_backend(config: dict):
|
||||||
"""Configure the terminal execution backend."""
|
"""Configure the terminal execution backend."""
|
||||||
import platform as _platform
|
import platform as _platform
|
||||||
import shutil
|
|
||||||
|
|
||||||
print_header("Terminal Backend")
|
print_header("Terminal Backend")
|
||||||
print_info("Choose where Hermes runs shell commands and code.")
|
print_info("Choose where Hermes runs shell commands and code.")
|
||||||
print_info("This affects tool execution, file access, and isolation.")
|
print_info("This affects tool execution, file access, and isolation.")
|
||||||
|
|
|
||||||
|
|
@ -2324,12 +2324,10 @@ def start_server(
|
||||||
)
|
)
|
||||||
|
|
||||||
if open_browser:
|
if open_browser:
|
||||||
import threading
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
def _open():
|
def _open():
|
||||||
import time as _t
|
time.sleep(1.0)
|
||||||
_t.sleep(1.0)
|
|
||||||
webbrowser.open(f"http://{host}:{port}")
|
webbrowser.open(f"http://{host}:{port}")
|
||||||
|
|
||||||
threading.Thread(target=_open, daemon=True).start()
|
threading.Thread(target=_open, daemon=True).start()
|
||||||
|
|
|
||||||
19
run_agent.py
19
run_agent.py
|
|
@ -1088,8 +1088,7 @@ class AIAgent:
|
||||||
_is_bedrock_anthropic = self.provider == "bedrock"
|
_is_bedrock_anthropic = self.provider == "bedrock"
|
||||||
if _is_bedrock_anthropic:
|
if _is_bedrock_anthropic:
|
||||||
from agent.anthropic_adapter import build_anthropic_bedrock_client
|
from agent.anthropic_adapter import build_anthropic_bedrock_client
|
||||||
import re as _re
|
_region_match = re.search(r"bedrock-runtime\.([a-z0-9-]+)\.", base_url or "")
|
||||||
_region_match = _re.search(r"bedrock-runtime\.([a-z0-9-]+)\.", base_url or "")
|
|
||||||
_br_region = _region_match.group(1) if _region_match else "us-east-1"
|
_br_region = _region_match.group(1) if _region_match else "us-east-1"
|
||||||
self._bedrock_region = _br_region
|
self._bedrock_region = _br_region
|
||||||
self._anthropic_client = build_anthropic_bedrock_client(_br_region)
|
self._anthropic_client = build_anthropic_bedrock_client(_br_region)
|
||||||
|
|
@ -1130,8 +1129,7 @@ class AIAgent:
|
||||||
elif self.api_mode == "bedrock_converse":
|
elif self.api_mode == "bedrock_converse":
|
||||||
# AWS Bedrock — uses boto3 directly, no OpenAI client needed.
|
# AWS Bedrock — uses boto3 directly, no OpenAI client needed.
|
||||||
# Region is extracted from the base_url or defaults to us-east-1.
|
# Region is extracted from the base_url or defaults to us-east-1.
|
||||||
import re as _re
|
_region_match = re.search(r"bedrock-runtime\.([a-z0-9-]+)\.", base_url or "")
|
||||||
_region_match = _re.search(r"bedrock-runtime\.([a-z0-9-]+)\.", base_url or "")
|
|
||||||
self._bedrock_region = _region_match.group(1) if _region_match else "us-east-1"
|
self._bedrock_region = _region_match.group(1) if _region_match else "us-east-1"
|
||||||
# Guardrail config — read from config.yaml at init time.
|
# Guardrail config — read from config.yaml at init time.
|
||||||
self._bedrock_guardrail_config = None
|
self._bedrock_guardrail_config = None
|
||||||
|
|
@ -1576,7 +1574,6 @@ class AIAgent:
|
||||||
"Falling back to auto-detection.",
|
"Falling back to auto-detection.",
|
||||||
_config_context_length,
|
_config_context_length,
|
||||||
)
|
)
|
||||||
import sys
|
|
||||||
print(
|
print(
|
||||||
f"\n⚠ Invalid model.context_length in config.yaml: {_config_context_length!r}\n"
|
f"\n⚠ Invalid model.context_length in config.yaml: {_config_context_length!r}\n"
|
||||||
f" Must be a plain integer (e.g. 256000, not '256K').\n"
|
f" Must be a plain integer (e.g. 256000, not '256K').\n"
|
||||||
|
|
@ -1618,7 +1615,6 @@ class AIAgent:
|
||||||
"Falling back to auto-detection.",
|
"Falling back to auto-detection.",
|
||||||
self.model, _cp_ctx,
|
self.model, _cp_ctx,
|
||||||
)
|
)
|
||||||
import sys
|
|
||||||
print(
|
print(
|
||||||
f"\n⚠ Invalid context_length for model {self.model!r} in custom_providers: {_cp_ctx!r}\n"
|
f"\n⚠ Invalid context_length for model {self.model!r} in custom_providers: {_cp_ctx!r}\n"
|
||||||
f" Must be a plain integer (e.g. 256000, not '256K').\n"
|
f" Must be a plain integer (e.g. 256000, not '256K').\n"
|
||||||
|
|
@ -1881,8 +1877,6 @@ class AIAgent:
|
||||||
change persists across turns (unlike fallback which is
|
change persists across turns (unlike fallback which is
|
||||||
turn-scoped).
|
turn-scoped).
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
import re as _re
|
|
||||||
from hermes_cli.providers import determine_api_mode
|
from hermes_cli.providers import determine_api_mode
|
||||||
|
|
||||||
# ── Determine api_mode if not provided ──
|
# ── Determine api_mode if not provided ──
|
||||||
|
|
@ -1900,7 +1894,7 @@ class AIAgent:
|
||||||
and isinstance(base_url, str)
|
and isinstance(base_url, str)
|
||||||
and base_url
|
and base_url
|
||||||
):
|
):
|
||||||
base_url = _re.sub(r"/v1/?$", "", base_url)
|
base_url = re.sub(r"/v1/?$", "", base_url)
|
||||||
|
|
||||||
old_model = self.model
|
old_model = self.model
|
||||||
old_provider = self.provider
|
old_provider = self.provider
|
||||||
|
|
@ -2916,7 +2910,7 @@ class AIAgent:
|
||||||
role = msg.get("role", "unknown")
|
role = msg.get("role", "unknown")
|
||||||
content = msg.get("content")
|
content = msg.get("content")
|
||||||
tool_calls_data = None
|
tool_calls_data = None
|
||||||
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
if hasattr(msg, "tool_calls") and isinstance(msg.tool_calls, list) and msg.tool_calls:
|
||||||
tool_calls_data = [
|
tool_calls_data = [
|
||||||
{"name": tc.function.name, "arguments": tc.function.arguments}
|
{"name": tc.function.name, "arguments": tc.function.arguments}
|
||||||
for tc in msg.tool_calls
|
for tc in msg.tool_calls
|
||||||
|
|
@ -3182,15 +3176,14 @@ class AIAgent:
|
||||||
<title> tag instead of dumping raw HTML. Falls back to a truncated
|
<title> tag instead of dumping raw HTML. Falls back to a truncated
|
||||||
str(error) for everything else.
|
str(error) for everything else.
|
||||||
"""
|
"""
|
||||||
import re as _re
|
|
||||||
raw = str(error)
|
raw = str(error)
|
||||||
|
|
||||||
# Cloudflare / proxy HTML pages: grab the <title> for a clean summary
|
# Cloudflare / proxy HTML pages: grab the <title> for a clean summary
|
||||||
if "<!DOCTYPE" in raw or "<html" in raw:
|
if "<!DOCTYPE" in raw or "<html" in raw:
|
||||||
m = _re.search(r"<title[^>]*>([^<]+)</title>", raw, _re.IGNORECASE)
|
m = re.search(r"<title[^>]*>([^<]+)</title>", raw, re.IGNORECASE)
|
||||||
title = m.group(1).strip() if m else "HTML error page (title not found)"
|
title = m.group(1).strip() if m else "HTML error page (title not found)"
|
||||||
# Also grab Cloudflare Ray ID if present
|
# Also grab Cloudflare Ray ID if present
|
||||||
ray = _re.search(r"Cloudflare Ray ID:\s*<strong[^>]*>([^<]+)</strong>", raw)
|
ray = re.search(r"Cloudflare Ray ID:\s*<strong[^>]*>([^<]+)</strong>", raw)
|
||||||
ray_id = ray.group(1).strip() if ray else None
|
ray_id = ray.group(1).strip() if ray else None
|
||||||
status_code = getattr(error, "status_code", None)
|
status_code = getattr(error, "status_code", None)
|
||||||
parts = []
|
parts = []
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ from aiohttp.test_utils import TestClient, TestServer
|
||||||
from gateway.config import PlatformConfig
|
from gateway.config import PlatformConfig
|
||||||
from gateway.platforms.api_server import APIServerAdapter, cors_middleware
|
from gateway.platforms.api_server import APIServerAdapter, cors_middleware
|
||||||
|
|
||||||
|
_MOD = "gateway.platforms.api_server"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
@ -83,10 +85,10 @@ class TestListJobs:
|
||||||
"""GET /api/jobs returns job list."""
|
"""GET /api/jobs returns job list."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_list", return_value=[SAMPLE_JOB]
|
f"{_MOD}._cron_list", return_value=[SAMPLE_JOB]
|
||||||
):
|
):
|
||||||
resp = await cli.get("/api/jobs")
|
resp = await cli.get("/api/jobs")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -104,10 +106,10 @@ class TestListJobs:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_list = MagicMock(return_value=[SAMPLE_JOB])
|
mock_list = MagicMock(return_value=[SAMPLE_JOB])
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_list", mock_list
|
f"{_MOD}._cron_list", mock_list
|
||||||
):
|
):
|
||||||
resp = await cli.get("/api/jobs?include_disabled=true")
|
resp = await cli.get("/api/jobs?include_disabled=true")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -119,10 +121,10 @@ class TestListJobs:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_list = MagicMock(return_value=[])
|
mock_list = MagicMock(return_value=[])
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_list", mock_list
|
f"{_MOD}._cron_list", mock_list
|
||||||
):
|
):
|
||||||
resp = await cli.get("/api/jobs")
|
resp = await cli.get("/api/jobs")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -140,10 +142,10 @@ class TestCreateJob:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_create = MagicMock(return_value=SAMPLE_JOB)
|
mock_create = MagicMock(return_value=SAMPLE_JOB)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_create", mock_create
|
f"{_MOD}._cron_create", mock_create
|
||||||
):
|
):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "test-job",
|
"name": "test-job",
|
||||||
|
|
@ -164,7 +166,7 @@ class TestCreateJob:
|
||||||
"""POST /api/jobs without name returns 400."""
|
"""POST /api/jobs without name returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"schedule": "*/5 * * * *",
|
"schedule": "*/5 * * * *",
|
||||||
"prompt": "do something",
|
"prompt": "do something",
|
||||||
|
|
@ -178,7 +180,7 @@ class TestCreateJob:
|
||||||
"""POST /api/jobs with name > 200 chars returns 400."""
|
"""POST /api/jobs with name > 200 chars returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "x" * 201,
|
"name": "x" * 201,
|
||||||
"schedule": "*/5 * * * *",
|
"schedule": "*/5 * * * *",
|
||||||
|
|
@ -192,7 +194,7 @@ class TestCreateJob:
|
||||||
"""POST /api/jobs with prompt > 5000 chars returns 400."""
|
"""POST /api/jobs with prompt > 5000 chars returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "test-job",
|
"name": "test-job",
|
||||||
"schedule": "*/5 * * * *",
|
"schedule": "*/5 * * * *",
|
||||||
|
|
@ -207,7 +209,7 @@ class TestCreateJob:
|
||||||
"""POST /api/jobs with repeat=0 returns 400."""
|
"""POST /api/jobs with repeat=0 returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "test-job",
|
"name": "test-job",
|
||||||
"schedule": "*/5 * * * *",
|
"schedule": "*/5 * * * *",
|
||||||
|
|
@ -222,7 +224,7 @@ class TestCreateJob:
|
||||||
"""POST /api/jobs without schedule returns 400."""
|
"""POST /api/jobs without schedule returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "test-job",
|
"name": "test-job",
|
||||||
})
|
})
|
||||||
|
|
@ -242,10 +244,10 @@ class TestGetJob:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_get = MagicMock(return_value=SAMPLE_JOB)
|
mock_get = MagicMock(return_value=SAMPLE_JOB)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_get", mock_get
|
f"{_MOD}._cron_get", mock_get
|
||||||
):
|
):
|
||||||
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -259,10 +261,10 @@ class TestGetJob:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_get = MagicMock(return_value=None)
|
mock_get = MagicMock(return_value=None)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_get", mock_get
|
f"{_MOD}._cron_get", mock_get
|
||||||
):
|
):
|
||||||
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 404
|
assert resp.status == 404
|
||||||
|
|
@ -272,7 +274,7 @@ class TestGetJob:
|
||||||
"""GET /api/jobs/{id} with non-hex id returns 400."""
|
"""GET /api/jobs/{id} with non-hex id returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.get("/api/jobs/not-a-valid-hex!")
|
resp = await cli.get("/api/jobs/not-a-valid-hex!")
|
||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
|
@ -291,10 +293,10 @@ class TestUpdateJob:
|
||||||
updated_job = {**SAMPLE_JOB, "name": "updated-name"}
|
updated_job = {**SAMPLE_JOB, "name": "updated-name"}
|
||||||
mock_update = MagicMock(return_value=updated_job)
|
mock_update = MagicMock(return_value=updated_job)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_update", mock_update
|
f"{_MOD}._cron_update", mock_update
|
||||||
):
|
):
|
||||||
resp = await cli.patch(
|
resp = await cli.patch(
|
||||||
f"/api/jobs/{VALID_JOB_ID}",
|
f"/api/jobs/{VALID_JOB_ID}",
|
||||||
|
|
@ -317,10 +319,10 @@ class TestUpdateJob:
|
||||||
updated_job = {**SAMPLE_JOB, "name": "new-name"}
|
updated_job = {**SAMPLE_JOB, "name": "new-name"}
|
||||||
mock_update = MagicMock(return_value=updated_job)
|
mock_update = MagicMock(return_value=updated_job)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_update", mock_update
|
f"{_MOD}._cron_update", mock_update
|
||||||
):
|
):
|
||||||
resp = await cli.patch(
|
resp = await cli.patch(
|
||||||
f"/api/jobs/{VALID_JOB_ID}",
|
f"/api/jobs/{VALID_JOB_ID}",
|
||||||
|
|
@ -342,7 +344,7 @@ class TestUpdateJob:
|
||||||
"""PATCH /api/jobs/{id} with only unknown fields returns 400."""
|
"""PATCH /api/jobs/{id} with only unknown fields returns 400."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.patch(
|
resp = await cli.patch(
|
||||||
f"/api/jobs/{VALID_JOB_ID}",
|
f"/api/jobs/{VALID_JOB_ID}",
|
||||||
json={"evil_field": "malicious"},
|
json={"evil_field": "malicious"},
|
||||||
|
|
@ -363,10 +365,10 @@ class TestDeleteJob:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_remove = MagicMock(return_value=True)
|
mock_remove = MagicMock(return_value=True)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_remove", mock_remove
|
f"{_MOD}._cron_remove", mock_remove
|
||||||
):
|
):
|
||||||
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -380,10 +382,10 @@ class TestDeleteJob:
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
mock_remove = MagicMock(return_value=False)
|
mock_remove = MagicMock(return_value=False)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_remove", mock_remove
|
f"{_MOD}._cron_remove", mock_remove
|
||||||
):
|
):
|
||||||
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 404
|
assert resp.status == 404
|
||||||
|
|
@ -401,10 +403,10 @@ class TestPauseJob:
|
||||||
paused_job = {**SAMPLE_JOB, "enabled": False}
|
paused_job = {**SAMPLE_JOB, "enabled": False}
|
||||||
mock_pause = MagicMock(return_value=paused_job)
|
mock_pause = MagicMock(return_value=paused_job)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_pause", mock_pause
|
f"{_MOD}._cron_pause", mock_pause
|
||||||
):
|
):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/pause")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/pause")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -426,10 +428,10 @@ class TestResumeJob:
|
||||||
resumed_job = {**SAMPLE_JOB, "enabled": True}
|
resumed_job = {**SAMPLE_JOB, "enabled": True}
|
||||||
mock_resume = MagicMock(return_value=resumed_job)
|
mock_resume = MagicMock(return_value=resumed_job)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_resume", mock_resume
|
f"{_MOD}._cron_resume", mock_resume
|
||||||
):
|
):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/resume")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/resume")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -451,10 +453,10 @@ class TestRunJob:
|
||||||
triggered_job = {**SAMPLE_JOB, "last_run": "2025-01-01T00:00:00Z"}
|
triggered_job = {**SAMPLE_JOB, "last_run": "2025-01-01T00:00:00Z"}
|
||||||
mock_trigger = MagicMock(return_value=triggered_job)
|
mock_trigger = MagicMock(return_value=triggered_job)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_trigger", mock_trigger
|
f"{_MOD}._cron_trigger", mock_trigger
|
||||||
):
|
):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/run")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/run")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -473,7 +475,7 @@ class TestAuthRequired:
|
||||||
"""GET /api/jobs without API key returns 401 when key is set."""
|
"""GET /api/jobs without API key returns 401 when key is set."""
|
||||||
app = _create_app(auth_adapter)
|
app = _create_app(auth_adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.get("/api/jobs")
|
resp = await cli.get("/api/jobs")
|
||||||
assert resp.status == 401
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
@ -482,7 +484,7 @@ class TestAuthRequired:
|
||||||
"""POST /api/jobs without API key returns 401 when key is set."""
|
"""POST /api/jobs without API key returns 401 when key is set."""
|
||||||
app = _create_app(auth_adapter)
|
app = _create_app(auth_adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "test", "schedule": "* * * * *",
|
"name": "test", "schedule": "* * * * *",
|
||||||
})
|
})
|
||||||
|
|
@ -493,7 +495,7 @@ class TestAuthRequired:
|
||||||
"""GET /api/jobs/{id} without API key returns 401 when key is set."""
|
"""GET /api/jobs/{id} without API key returns 401 when key is set."""
|
||||||
app = _create_app(auth_adapter)
|
app = _create_app(auth_adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 401
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
@ -502,7 +504,7 @@ class TestAuthRequired:
|
||||||
"""DELETE /api/jobs/{id} without API key returns 401."""
|
"""DELETE /api/jobs/{id} without API key returns 401."""
|
||||||
app = _create_app(auth_adapter)
|
app = _create_app(auth_adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True):
|
with patch(f"{_MOD}._CRON_AVAILABLE", True):
|
||||||
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 401
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
@ -512,10 +514,10 @@ class TestAuthRequired:
|
||||||
app = _create_app(auth_adapter)
|
app = _create_app(auth_adapter)
|
||||||
mock_list = MagicMock(return_value=[])
|
mock_list = MagicMock(return_value=[])
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(
|
with patch(
|
||||||
APIServerAdapter, "_CRON_AVAILABLE", True
|
f"{_MOD}._CRON_AVAILABLE", True
|
||||||
), patch.object(
|
), patch(
|
||||||
APIServerAdapter, "_cron_list", mock_list
|
f"{_MOD}._cron_list", mock_list
|
||||||
):
|
):
|
||||||
resp = await cli.get(
|
resp = await cli.get(
|
||||||
"/api/jobs",
|
"/api/jobs",
|
||||||
|
|
@ -534,7 +536,7 @@ class TestCronUnavailable:
|
||||||
"""GET /api/jobs returns 501 when _CRON_AVAILABLE is False."""
|
"""GET /api/jobs returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.get("/api/jobs")
|
resp = await cli.get("/api/jobs")
|
||||||
assert resp.status == 501
|
assert resp.status == 501
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
|
@ -551,8 +553,8 @@ class TestCronUnavailable:
|
||||||
return SAMPLE_JOB
|
return SAMPLE_JOB
|
||||||
|
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True), patch.object(
|
with patch(f"{_MOD}._CRON_AVAILABLE", True), patch(
|
||||||
APIServerAdapter, "_cron_pause", staticmethod(_plain_pause)
|
f"{_MOD}._cron_pause", _plain_pause
|
||||||
):
|
):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/pause")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/pause")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -571,8 +573,8 @@ class TestCronUnavailable:
|
||||||
return [SAMPLE_JOB]
|
return [SAMPLE_JOB]
|
||||||
|
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True), patch.object(
|
with patch(f"{_MOD}._CRON_AVAILABLE", True), patch(
|
||||||
APIServerAdapter, "_cron_list", staticmethod(_plain_list)
|
f"{_MOD}._cron_list", _plain_list
|
||||||
):
|
):
|
||||||
resp = await cli.get("/api/jobs?include_disabled=true")
|
resp = await cli.get("/api/jobs?include_disabled=true")
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
@ -593,8 +595,8 @@ class TestCronUnavailable:
|
||||||
return updated_job
|
return updated_job
|
||||||
|
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", True), patch.object(
|
with patch(f"{_MOD}._CRON_AVAILABLE", True), patch(
|
||||||
APIServerAdapter, "_cron_update", staticmethod(_plain_update)
|
f"{_MOD}._cron_update", _plain_update
|
||||||
):
|
):
|
||||||
resp = await cli.patch(
|
resp = await cli.patch(
|
||||||
f"/api/jobs/{VALID_JOB_ID}",
|
f"/api/jobs/{VALID_JOB_ID}",
|
||||||
|
|
@ -611,7 +613,7 @@ class TestCronUnavailable:
|
||||||
"""POST /api/jobs returns 501 when _CRON_AVAILABLE is False."""
|
"""POST /api/jobs returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.post("/api/jobs", json={
|
resp = await cli.post("/api/jobs", json={
|
||||||
"name": "test", "schedule": "* * * * *",
|
"name": "test", "schedule": "* * * * *",
|
||||||
})
|
})
|
||||||
|
|
@ -622,7 +624,7 @@ class TestCronUnavailable:
|
||||||
"""GET /api/jobs/{id} returns 501 when _CRON_AVAILABLE is False."""
|
"""GET /api/jobs/{id} returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.get(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 501
|
assert resp.status == 501
|
||||||
|
|
||||||
|
|
@ -631,7 +633,7 @@ class TestCronUnavailable:
|
||||||
"""DELETE /api/jobs/{id} returns 501 when _CRON_AVAILABLE is False."""
|
"""DELETE /api/jobs/{id} returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
resp = await cli.delete(f"/api/jobs/{VALID_JOB_ID}")
|
||||||
assert resp.status == 501
|
assert resp.status == 501
|
||||||
|
|
||||||
|
|
@ -640,7 +642,7 @@ class TestCronUnavailable:
|
||||||
"""POST /api/jobs/{id}/pause returns 501 when _CRON_AVAILABLE is False."""
|
"""POST /api/jobs/{id}/pause returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/pause")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/pause")
|
||||||
assert resp.status == 501
|
assert resp.status == 501
|
||||||
|
|
||||||
|
|
@ -649,7 +651,7 @@ class TestCronUnavailable:
|
||||||
"""POST /api/jobs/{id}/resume returns 501 when _CRON_AVAILABLE is False."""
|
"""POST /api/jobs/{id}/resume returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/resume")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/resume")
|
||||||
assert resp.status == 501
|
assert resp.status == 501
|
||||||
|
|
||||||
|
|
@ -658,6 +660,6 @@ class TestCronUnavailable:
|
||||||
"""POST /api/jobs/{id}/run returns 501 when _CRON_AVAILABLE is False."""
|
"""POST /api/jobs/{id}/run returns 501 when _CRON_AVAILABLE is False."""
|
||||||
app = _create_app(adapter)
|
app = _create_app(adapter)
|
||||||
async with TestClient(TestServer(app)) as cli:
|
async with TestClient(TestServer(app)) as cli:
|
||||||
with patch.object(APIServerAdapter, "_CRON_AVAILABLE", False):
|
with patch(f"{_MOD}._CRON_AVAILABLE", False):
|
||||||
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/run")
|
resp = await cli.post(f"/api/jobs/{VALID_JOB_ID}/run")
|
||||||
assert resp.status == 501
|
assert resp.status == 501
|
||||||
|
|
|
||||||
|
|
@ -1911,7 +1911,6 @@ def _maybe_start_recording(task_id: str):
|
||||||
recordings_dir.mkdir(parents=True, exist_ok=True)
|
recordings_dir.mkdir(parents=True, exist_ok=True)
|
||||||
_cleanup_old_recordings(max_age_hours=72)
|
_cleanup_old_recordings(max_age_hours=72)
|
||||||
|
|
||||||
import time
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||||
recording_path = recordings_dir / f"session_{timestamp}_{task_id[:16]}.webm"
|
recording_path = recordings_dir / f"session_{timestamp}_{task_id[:16]}.webm"
|
||||||
|
|
||||||
|
|
@ -2027,8 +2026,6 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import uuid as uuid_mod
|
import uuid as uuid_mod
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
effective_task_id = task_id or "default"
|
effective_task_id = task_id or "default"
|
||||||
|
|
||||||
# Save screenshot to persistent location so it can be shared with users
|
# Save screenshot to persistent location so it can be shared with users
|
||||||
|
|
@ -2210,7 +2207,6 @@ def _cleanup_old_screenshots(screenshots_dir, max_age_hours=24):
|
||||||
|
|
||||||
def _cleanup_old_recordings(max_age_hours=72):
|
def _cleanup_old_recordings(max_age_hours=72):
|
||||||
"""Remove browser recordings older than max_age_hours to prevent disk bloat."""
|
"""Remove browser recordings older than max_age_hours to prevent disk bloat."""
|
||||||
import time
|
|
||||||
try:
|
try:
|
||||||
hermes_home = get_hermes_home()
|
hermes_home = get_hermes_home()
|
||||||
recordings_dir = hermes_home / "browser_recordings"
|
recordings_dir = hermes_home / "browser_recordings"
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,6 @@ class CheckpointManager:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_shortstat(stat_line: str, entry: Dict) -> None:
|
def _parse_shortstat(stat_line: str, entry: Dict) -> None:
|
||||||
"""Parse git --shortstat output into entry dict."""
|
"""Parse git --shortstat output into entry dict."""
|
||||||
import re
|
|
||||||
m = re.search(r'(\d+) file', stat_line)
|
m = re.search(r'(\d+) file', stat_line)
|
||||||
if m:
|
if m:
|
||||||
entry["files_changed"] = int(m.group(1))
|
entry["files_changed"] = int(m.group(1))
|
||||||
|
|
|
||||||
|
|
@ -1540,7 +1540,6 @@ def _interrupted_call_result() -> str:
|
||||||
def _interpolate_env_vars(value):
|
def _interpolate_env_vars(value):
|
||||||
"""Recursively resolve ``${VAR}`` placeholders from ``os.environ``."""
|
"""Recursively resolve ``${VAR}`` placeholders from ``os.environ``."""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
import re
|
|
||||||
def _replace(m):
|
def _replace(m):
|
||||||
return os.environ.get(m.group(1), m.group(0))
|
return os.environ.get(m.group(1), m.group(0))
|
||||||
return re.sub(r"\$\{([^}]+)\}", _replace, value)
|
return re.sub(r"\$\{([^}]+)\}", _replace, value)
|
||||||
|
|
|
||||||
|
|
@ -1167,32 +1167,31 @@ PROCESS_SCHEMA = {
|
||||||
|
|
||||||
|
|
||||||
def _handle_process(args, **kw):
|
def _handle_process(args, **kw):
|
||||||
import json as _json
|
|
||||||
task_id = kw.get("task_id")
|
task_id = kw.get("task_id")
|
||||||
action = args.get("action", "")
|
action = args.get("action", "")
|
||||||
# Coerce to string — some models send session_id as an integer
|
# Coerce to string — some models send session_id as an integer
|
||||||
session_id = str(args.get("session_id", "")) if args.get("session_id") is not None else ""
|
session_id = str(args.get("session_id", "")) if args.get("session_id") is not None else ""
|
||||||
|
|
||||||
if action == "list":
|
if action == "list":
|
||||||
return _json.dumps({"processes": process_registry.list_sessions(task_id=task_id)}, ensure_ascii=False)
|
return json.dumps({"processes": process_registry.list_sessions(task_id=task_id)}, ensure_ascii=False)
|
||||||
elif action in ("poll", "log", "wait", "kill", "write", "submit", "close"):
|
elif action in ("poll", "log", "wait", "kill", "write", "submit", "close"):
|
||||||
if not session_id:
|
if not session_id:
|
||||||
return tool_error(f"session_id is required for {action}")
|
return tool_error(f"session_id is required for {action}")
|
||||||
if action == "poll":
|
if action == "poll":
|
||||||
return _json.dumps(process_registry.poll(session_id), ensure_ascii=False)
|
return json.dumps(process_registry.poll(session_id), ensure_ascii=False)
|
||||||
elif action == "log":
|
elif action == "log":
|
||||||
return _json.dumps(process_registry.read_log(
|
return json.dumps(process_registry.read_log(
|
||||||
session_id, offset=args.get("offset", 0), limit=args.get("limit", 200)), ensure_ascii=False)
|
session_id, offset=args.get("offset", 0), limit=args.get("limit", 200)), ensure_ascii=False)
|
||||||
elif action == "wait":
|
elif action == "wait":
|
||||||
return _json.dumps(process_registry.wait(session_id, timeout=args.get("timeout")), ensure_ascii=False)
|
return json.dumps(process_registry.wait(session_id, timeout=args.get("timeout")), ensure_ascii=False)
|
||||||
elif action == "kill":
|
elif action == "kill":
|
||||||
return _json.dumps(process_registry.kill_process(session_id), ensure_ascii=False)
|
return json.dumps(process_registry.kill_process(session_id), ensure_ascii=False)
|
||||||
elif action == "write":
|
elif action == "write":
|
||||||
return _json.dumps(process_registry.write_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
|
return json.dumps(process_registry.write_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
|
||||||
elif action == "submit":
|
elif action == "submit":
|
||||||
return _json.dumps(process_registry.submit_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
|
return json.dumps(process_registry.submit_stdin(session_id, str(args.get("data", ""))), ensure_ascii=False)
|
||||||
elif action == "close":
|
elif action == "close":
|
||||||
return _json.dumps(process_registry.close_stdin(session_id), ensure_ascii=False)
|
return json.dumps(process_registry.close_stdin(session_id), ensure_ascii=False)
|
||||||
return tool_error(f"Unknown process action: {action}. Use: list, poll, log, wait, kill, write, submit, close")
|
return tool_error(f"Unknown process action: {action}. Use: list, poll, log, wait, kill, write, submit, close")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,6 @@ def _get_disabled_skill_names() -> Set[str]:
|
||||||
|
|
||||||
def _is_skill_disabled(name: str, platform: str = None) -> bool:
|
def _is_skill_disabled(name: str, platform: str = None) -> bool:
|
||||||
"""Check if a skill is disabled in config."""
|
"""Check if a skill is disabled in config."""
|
||||||
import os
|
|
||||||
try:
|
try:
|
||||||
from hermes_cli.config import load_config
|
from hermes_cli.config import load_config
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,6 @@ def _prompt_for_sudo_password(timeout_seconds: int = 45) -> str:
|
||||||
directly from /dev/tty with echo disabled.
|
directly from /dev/tty with echo disabled.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import time as time_module
|
|
||||||
|
|
||||||
# Use the registered callback when available (prompt_toolkit-compatible)
|
# Use the registered callback when available (prompt_toolkit-compatible)
|
||||||
if _sudo_password_callback is not None:
|
if _sudo_password_callback is not None:
|
||||||
|
|
@ -278,7 +277,7 @@ def _prompt_for_sudo_password(timeout_seconds: int = 45) -> str:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.environ["HERMES_SPINNER_PAUSE"] = "1"
|
os.environ["HERMES_SPINNER_PAUSE"] = "1"
|
||||||
time_module.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("┌" + "─" * 58 + "┐")
|
print("┌" + "─" * 58 + "┐")
|
||||||
|
|
|
||||||
105
uv.lock
generated
105
uv.lock
generated
|
|
@ -426,7 +426,7 @@ wheels = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atroposlib"
|
name = "atroposlib"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = { git = "https://github.com/NousResearch/atropos.git#c421582b6f7ce8a32f751aab3117d3824ac8f709" }
|
source = { git = "https://github.com/NousResearch/atropos.git?rev=c20c85256e5a45ad31edf8b7276e9c5ee1995a30#c20c85256e5a45ad31edf8b7276e9c5ee1995a30" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|
@ -558,6 +558,34 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boto3"
|
||||||
|
version = "1.42.92"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore" },
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "s3transfer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/3b/84cafa37e85a57618554bd2bc21bd569417097f45f18c23ef488e6c69683/boto3-1.42.92.tar.gz", hash = "sha256:55ec6ef6fc81f46d567a7d1d398d1e5c375d468905d0ccd9e1f767f0c77dbe9b", size = 113207, upload-time = "2026-04-20T19:38:17.293Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/8f/350ffd50aaa515429464deb1dc85893a21a64cb41892feb6b22ce87304ad/boto3-1.42.92-py3-none-any.whl", hash = "sha256:c90d9a170faa0585755fa103a3cd9595e1f53443864e902c180f3d8177589125", size = 140555, upload-time = "2026-04-20T19:38:14.323Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "botocore"
|
||||||
|
version = "1.42.92"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d5/0a/6785ce224ba4483b3e1282d959e1dd2c2898823336f013464c43cb154036/botocore-1.42.92.tar.gz", hash = "sha256:f1193d3057a2d0267353d7ef4e136be37ea432336d097fcb1951fae566ca3a22", size = 15235239, upload-time = "2026-04-20T19:38:05.085Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/b8/41d4d7ba75a4fb4f11362e96371a12695bc6ba0bb7cc680137db0213f97e/botocore-1.42.92-py3-none-any.whl", hash = "sha256:09ddefddbb1565ceef4b44b4b6e61b1ca5f12701d1494ecc85c1133d1b1e81fb", size = 14916275, upload-time = "2026-04-20T19:38:01.684Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "5.5.2"
|
version = "5.5.2"
|
||||||
|
|
@ -1838,7 +1866,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermes-agent"
|
name = "hermes-agent"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anthropic" },
|
{ name = "anthropic" },
|
||||||
|
|
@ -1871,6 +1899,7 @@ all = [
|
||||||
{ name = "aiosqlite", marker = "sys_platform == 'linux'" },
|
{ name = "aiosqlite", marker = "sys_platform == 'linux'" },
|
||||||
{ name = "alibabacloud-dingtalk" },
|
{ name = "alibabacloud-dingtalk" },
|
||||||
{ name = "asyncpg", marker = "sys_platform == 'linux'" },
|
{ name = "asyncpg", marker = "sys_platform == 'linux'" },
|
||||||
|
{ name = "boto3" },
|
||||||
{ name = "croniter" },
|
{ name = "croniter" },
|
||||||
{ name = "daytona" },
|
{ name = "daytona" },
|
||||||
{ name = "debugpy" },
|
{ name = "debugpy" },
|
||||||
|
|
@ -1893,12 +1922,16 @@ all = [
|
||||||
{ name = "pytest-xdist" },
|
{ name = "pytest-xdist" },
|
||||||
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
||||||
{ name = "pywinpty", marker = "sys_platform == 'win32'" },
|
{ name = "pywinpty", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "qrcode" },
|
||||||
{ name = "simple-term-menu" },
|
{ name = "simple-term-menu" },
|
||||||
{ name = "slack-bolt" },
|
{ name = "slack-bolt" },
|
||||||
{ name = "slack-sdk" },
|
{ name = "slack-sdk" },
|
||||||
{ name = "sounddevice" },
|
{ name = "sounddevice" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
bedrock = [
|
||||||
|
{ name = "boto3" },
|
||||||
|
]
|
||||||
cli = [
|
cli = [
|
||||||
{ name = "simple-term-menu" },
|
{ name = "simple-term-menu" },
|
||||||
]
|
]
|
||||||
|
|
@ -1918,9 +1951,11 @@ dev = [
|
||||||
dingtalk = [
|
dingtalk = [
|
||||||
{ name = "alibabacloud-dingtalk" },
|
{ name = "alibabacloud-dingtalk" },
|
||||||
{ name = "dingtalk-stream" },
|
{ name = "dingtalk-stream" },
|
||||||
|
{ name = "qrcode" },
|
||||||
]
|
]
|
||||||
feishu = [
|
feishu = [
|
||||||
{ name = "lark-oapi" },
|
{ name = "lark-oapi" },
|
||||||
|
{ name = "qrcode" },
|
||||||
]
|
]
|
||||||
homeassistant = [
|
homeassistant = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|
@ -1941,6 +1976,7 @@ messaging = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
{ name = "discord-py", extra = ["voice"] },
|
{ name = "discord-py", extra = ["voice"] },
|
||||||
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
||||||
|
{ name = "qrcode" },
|
||||||
{ name = "slack-bolt" },
|
{ name = "slack-bolt" },
|
||||||
{ name = "slack-sdk" },
|
{ name = "slack-sdk" },
|
||||||
]
|
]
|
||||||
|
|
@ -1974,6 +2010,7 @@ termux = [
|
||||||
{ name = "honcho-ai" },
|
{ name = "honcho-ai" },
|
||||||
{ name = "mcp" },
|
{ name = "mcp" },
|
||||||
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
|
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
|
||||||
|
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
||||||
{ name = "pywinpty", marker = "sys_platform == 'win32'" },
|
{ name = "pywinpty", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "simple-term-menu" },
|
{ name = "simple-term-menu" },
|
||||||
]
|
]
|
||||||
|
|
@ -2003,7 +2040,8 @@ requires-dist = [
|
||||||
{ name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = ">=2.0.0" },
|
{ name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = ">=2.0.0" },
|
||||||
{ name = "anthropic", specifier = ">=0.39.0,<1" },
|
{ name = "anthropic", specifier = ">=0.39.0,<1" },
|
||||||
{ name = "asyncpg", marker = "extra == 'matrix'", specifier = ">=0.29" },
|
{ name = "asyncpg", marker = "extra == 'matrix'", specifier = ">=0.29" },
|
||||||
{ name = "atroposlib", marker = "extra == 'rl'", git = "https://github.com/NousResearch/atropos.git" },
|
{ name = "atroposlib", marker = "extra == 'rl'", git = "https://github.com/NousResearch/atropos.git?rev=c20c85256e5a45ad31edf8b7276e9c5ee1995a30" },
|
||||||
|
{ name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.35.0,<2" },
|
||||||
{ name = "croniter", marker = "extra == 'cron'", specifier = ">=6.0.0,<7" },
|
{ name = "croniter", marker = "extra == 'cron'", specifier = ">=6.0.0,<7" },
|
||||||
{ name = "daytona", marker = "extra == 'daytona'", specifier = ">=0.148.0,<1" },
|
{ name = "daytona", marker = "extra == 'daytona'", specifier = ">=0.148.0,<1" },
|
||||||
{ name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.0,<2" },
|
{ name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.0,<2" },
|
||||||
|
|
@ -2020,6 +2058,7 @@ requires-dist = [
|
||||||
{ name = "firecrawl-py", specifier = ">=4.16.0,<5" },
|
{ name = "firecrawl-py", specifier = ">=4.16.0,<5" },
|
||||||
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
|
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
|
||||||
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'termux'" },
|
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'termux'" },
|
||||||
|
{ name = "hermes-agent", extras = ["bedrock"], marker = "extra == 'all'" },
|
||||||
{ name = "hermes-agent", extras = ["cli"], marker = "extra == 'all'" },
|
{ name = "hermes-agent", extras = ["cli"], marker = "extra == 'all'" },
|
||||||
{ name = "hermes-agent", extras = ["cli"], marker = "extra == 'termux'" },
|
{ name = "hermes-agent", extras = ["cli"], marker = "extra == 'termux'" },
|
||||||
{ name = "hermes-agent", extras = ["cron"], marker = "extra == 'all'" },
|
{ name = "hermes-agent", extras = ["cron"], marker = "extra == 'all'" },
|
||||||
|
|
@ -2066,8 +2105,12 @@ requires-dist = [
|
||||||
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.0,<4" },
|
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.0,<4" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.1,<2" },
|
{ name = "python-dotenv", specifier = ">=1.2.1,<2" },
|
||||||
{ name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'messaging'", specifier = ">=22.6,<23" },
|
{ name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'messaging'", specifier = ">=22.6,<23" },
|
||||||
|
{ name = "python-telegram-bot", extras = ["webhooks"], marker = "extra == 'termux'", specifier = ">=22.6,<23" },
|
||||||
{ name = "pywinpty", marker = "sys_platform == 'win32' and extra == 'pty'", specifier = ">=2.0.0,<3" },
|
{ name = "pywinpty", marker = "sys_platform == 'win32' and extra == 'pty'", specifier = ">=2.0.0,<3" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.2,<7" },
|
{ name = "pyyaml", specifier = ">=6.0.2,<7" },
|
||||||
|
{ name = "qrcode", marker = "extra == 'dingtalk'", specifier = ">=7.0,<8" },
|
||||||
|
{ name = "qrcode", marker = "extra == 'feishu'", specifier = ">=7.0,<8" },
|
||||||
|
{ name = "qrcode", marker = "extra == 'messaging'", specifier = ">=7.0,<8" },
|
||||||
{ name = "requests", specifier = ">=2.33.0,<3" },
|
{ name = "requests", specifier = ">=2.33.0,<3" },
|
||||||
{ name = "rich", specifier = ">=14.3.3,<15" },
|
{ name = "rich", specifier = ">=14.3.3,<15" },
|
||||||
{ name = "simple-term-menu", marker = "extra == 'cli'", specifier = ">=1.0,<2" },
|
{ name = "simple-term-menu", marker = "extra == 'cli'", specifier = ">=1.0,<2" },
|
||||||
|
|
@ -2077,13 +2120,13 @@ requires-dist = [
|
||||||
{ name = "slack-sdk", marker = "extra == 'slack'", specifier = ">=3.27.0,<4" },
|
{ name = "slack-sdk", marker = "extra == 'slack'", specifier = ">=3.27.0,<4" },
|
||||||
{ name = "sounddevice", marker = "extra == 'voice'", specifier = ">=0.4.6,<1" },
|
{ name = "sounddevice", marker = "extra == 'voice'", specifier = ">=0.4.6,<1" },
|
||||||
{ name = "tenacity", specifier = ">=9.1.4,<10" },
|
{ name = "tenacity", specifier = ">=9.1.4,<10" },
|
||||||
{ name = "tinker", marker = "extra == 'rl'", git = "https://github.com/thinking-machines-lab/tinker.git" },
|
{ name = "tinker", marker = "extra == 'rl'", git = "https://github.com/thinking-machines-lab/tinker.git?rev=30517b667f18a3dfb7ef33fb56cf686d5820ba2b" },
|
||||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'rl'", specifier = ">=0.24.0,<1" },
|
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'rl'", specifier = ">=0.24.0,<1" },
|
||||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = ">=0.24.0,<1" },
|
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = ">=0.24.0,<1" },
|
||||||
{ name = "wandb", marker = "extra == 'rl'", specifier = ">=0.15.0,<1" },
|
{ name = "wandb", marker = "extra == 'rl'", specifier = ">=0.15.0,<1" },
|
||||||
{ name = "yc-bench", marker = "python_full_version >= '3.12' and extra == 'yc-bench'", git = "https://github.com/collinear-ai/yc-bench.git" },
|
{ name = "yc-bench", marker = "python_full_version >= '3.12' and extra == 'yc-bench'", git = "https://github.com/collinear-ai/yc-bench.git?rev=bfb0c88062450f46341bd9a5298903fc2e952a5c" },
|
||||||
]
|
]
|
||||||
provides-extras = ["modal", "daytona", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "acp", "mistral", "termux", "dingtalk", "feishu", "web", "rl", "yc-bench", "all"]
|
provides-extras = ["modal", "daytona", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "acp", "mistral", "bedrock", "termux", "dingtalk", "feishu", "web", "rl", "yc-bench", "all"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hf-transfer"
|
name = "hf-transfer"
|
||||||
|
|
@ -2410,6 +2453,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jmespath"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "joblib"
|
name = "joblib"
|
||||||
version = "1.5.3"
|
version = "1.5.3"
|
||||||
|
|
@ -4109,6 +4161,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pypng"
|
||||||
|
version = "0.20220715.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/93/cd/112f092ec27cca83e0516de0a3368dbd9128c187fb6b52aaaa7cde39c96d/pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1", size = 128992, upload-time = "2022-07-15T14:11:05.301Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/b9/3766cc361d93edb2ce81e2e1f87dd98f314d7d513877a342d31b30741680/pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", size = 58057, upload-time = "2022-07-15T14:11:03.713Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.2"
|
||||||
|
|
@ -4311,6 +4372,20 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode"
|
||||||
|
version = "7.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "pypng" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/30/35/ad6d4c5a547fe9a5baf85a9edbafff93fc6394b014fab30595877305fa59/qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845", size = 535974, upload-time = "2023-02-05T22:11:46.548Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/79/aaf0c1c7214f2632badb2771d770b1500d3d7cbdf2590ae62e721ec50584/qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", size = 46197, upload-time = "2023-02-05T22:11:43.4Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "referencing"
|
name = "referencing"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
|
|
@ -4577,6 +4652,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "s3transfer"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "safetensors"
|
name = "safetensors"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -4927,8 +5014,8 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinker"
|
name = "tinker"
|
||||||
version = "0.16.1"
|
version = "0.18.0"
|
||||||
source = { git = "https://github.com/thinking-machines-lab/tinker.git#07bd3c2dd3cd4398ac1c26f0ec0deccbf3c1f913" }
|
source = { git = "https://github.com/thinking-machines-lab/tinker.git?rev=30517b667f18a3dfb7ef33fb56cf686d5820ba2b#30517b667f18a3dfb7ef33fb56cf686d5820ba2b" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|
@ -5653,7 +5740,7 @@ wheels = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yc-bench"
|
name = "yc-bench"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { git = "https://github.com/collinear-ai/yc-bench.git#0c53c98f01a431db2e391482bc46013045854ab2" }
|
source = { git = "https://github.com/collinear-ai/yc-bench.git?rev=bfb0c88062450f46341bd9a5298903fc2e952a5c#bfb0c88062450f46341bd9a5298903fc2e952a5c" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "litellm", marker = "python_full_version >= '3.12'" },
|
{ name = "litellm", marker = "python_full_version >= '3.12'" },
|
||||||
{ name = "matplotlib", marker = "python_full_version >= '3.12'" },
|
{ name = "matplotlib", marker = "python_full_version >= '3.12'" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue