From e032faaeff81d7e4fa39659badbacc7b4004dc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20S=C3=A1nchez=20Vall=C3=A9s?= Date: Wed, 4 Mar 2026 20:04:00 +0100 Subject: [PATCH] Merge branch 'main' of upstream/main into fix/test-failures --- .gitignore | 2 +- nanobot/agent/tools/base.py | 2 ++ nanobot/agent/tools/cron.py | 5 ++++- nanobot/agent/tools/filesystem.py | 11 +++++++++++ nanobot/channels/feishu.py | 6 ++++++ nanobot/providers/openai_codex_provider.py | 3 +++ pyproject.toml | 2 ++ tests/test_context_prompt_cache.py | 1 + 8 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d7b930d..742d593 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ __pycache__/ poetry.lock .pytest_cache/ botpy.log -tests/ + diff --git a/nanobot/agent/tools/base.py b/nanobot/agent/tools/base.py index 8dd82c7..051fc9a 100644 --- a/nanobot/agent/tools/base.py +++ b/nanobot/agent/tools/base.py @@ -54,6 +54,8 @@ class Tool(ABC): def validate_params(self, params: dict[str, Any]) -> list[str]: """Validate tool parameters against JSON schema. Returns error list (empty if valid).""" + if not isinstance(params, dict): + return [f"parameters must be an object, got {type(params).__name__}"] schema = self.parameters or {} if schema.get("type", "object") != "object": raise ValueError(f"Schema must be object type, got {schema.get('type')!r}") diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index 13b1e12..f8e737b 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -122,7 +122,10 @@ class CronTool(Tool): elif at: from datetime import datetime - dt = datetime.fromisoformat(at) + try: + dt = datetime.fromisoformat(at) + except ValueError: + return f"Error: invalid ISO datetime format '{at}'. Expected format: YYYY-MM-DDTHH:MM:SS" at_ms = int(dt.timestamp() * 1000) schedule = CronSchedule(kind="at", at_ms=at_ms) delete_after = True diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index bbdd49c..7b0b867 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -26,6 +26,8 @@ def _resolve_path( class ReadFileTool(Tool): """Tool to read file contents.""" + _MAX_CHARS = 128_000 # ~128 KB — prevents OOM from reading huge files into LLM context + def __init__(self, workspace: Path | None = None, allowed_dir: Path | None = None): self._workspace = workspace self._allowed_dir = allowed_dir @@ -54,7 +56,16 @@ class ReadFileTool(Tool): if not file_path.is_file(): return f"Error: Not a file: {path}" + size = file_path.stat().st_size + if size > self._MAX_CHARS * 4: # rough upper bound (UTF-8 chars ≤ 4 bytes) + return ( + f"Error: File too large ({size:,} bytes). " + f"Use exec tool with head/tail/grep to read portions." + ) + content = file_path.read_text(encoding="utf-8") + if len(content) > self._MAX_CHARS: + return content[: self._MAX_CHARS] + f"\n\n... (truncated — file is {len(content):,} chars, limit {self._MAX_CHARS:,})" return content except PermissionError as e: return f"Error: {e}" diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 7d26fa8..0cd84c3 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -519,6 +519,12 @@ class FeishuChannel(BaseChannel): ) -> tuple[bytes | None, str | None]: """Download a file/audio/media from a Feishu message by message_id and file_key.""" from lark_oapi.api.im.v1 import GetMessageResourceRequest + + # Feishu API only accepts 'image' or 'file' as type parameter + # Convert 'audio' to 'file' for API compatibility + if resource_type == "audio": + resource_type = "file" + try: request = ( GetMessageResourceRequest.builder() diff --git a/nanobot/providers/openai_codex_provider.py b/nanobot/providers/openai_codex_provider.py index b6afa65..d04e210 100644 --- a/nanobot/providers/openai_codex_provider.py +++ b/nanobot/providers/openai_codex_provider.py @@ -52,6 +52,9 @@ class OpenAICodexProvider(LLMProvider): "parallel_tool_calls": True, } + if reasoning_effort: + body["reasoning"] = {"effort": reasoning_effort} + if tools: body["tools"] = _convert_tools(tools) diff --git a/pyproject.toml b/pyproject.toml index d384f3f..e5214bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ dependencies = [ "mcp>=1.26.0,<2.0.0", "json-repair>=0.57.0,<1.0.0", "chardet>=3.0.2,<6.0.0", + "openai>=2.8.0", + ] [project.optional-dependencies] diff --git a/tests/test_context_prompt_cache.py b/tests/test_context_prompt_cache.py index fa7f02d..d347e53 100644 --- a/tests/test_context_prompt_cache.py +++ b/tests/test_context_prompt_cache.py @@ -55,6 +55,7 @@ def test_runtime_context_is_separate_untrusted_user_message(tmp_path) -> None: assert "## Current Session" not in messages[0]["content"] assert len(messages) == 2 + # Runtime context is now merged with user message into a single message assert messages[-1]["role"] == "user" user_content = messages[-1]["content"]