fix(python): harden zeroclaw-tools CLI and integration ergonomics

This commit is contained in:
Chummy 2026-02-17 15:49:01 +08:00
parent e5ef8a3b62
commit f01d38be35
10 changed files with 110 additions and 27 deletions

View file

@ -6,6 +6,7 @@ import argparse
import asyncio
import os
import sys
from typing import Optional
from langchain_core.messages import HumanMessage
@ -25,7 +26,7 @@ DEFAULT_SYSTEM_PROMPT = """You are ZeroClaw, an AI assistant with full system ac
Be concise and helpful. Execute tools directly without excessive explanation."""
async def chat(message: str, api_key: str, base_url: str, model: str) -> str:
async def chat(message: str, api_key: str, base_url: Optional[str], model: str) -> str:
"""Run a single chat message through the agent."""
agent = create_agent(
tools=[shell, file_read, file_write, web_search, http_request, memory_store, memory_recall],
@ -39,21 +40,40 @@ async def chat(message: str, api_key: str, base_url: str, model: str) -> str:
return result["messages"][-1].content or "Done."
def main():
"""CLI main entry point."""
def _build_parser() -> argparse.ArgumentParser:
"""Build CLI argument parser."""
parser = argparse.ArgumentParser(
description="ZeroClaw Tools - LangGraph-based tool calling for LLMs"
)
parser.add_argument("message", nargs="+", help="Message to send to the agent")
parser.add_argument(
"message",
nargs="*",
help="Message to send to the agent (optional in interactive mode)",
)
parser.add_argument("--model", "-m", default="glm-5", help="Model to use")
parser.add_argument("--api-key", "-k", default=None, help="API key")
parser.add_argument("--base-url", "-u", default=None, help="API base URL")
parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode")
return parser
args = parser.parse_args()
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse CLI arguments and enforce mode-specific requirements."""
parser = _build_parser()
args = parser.parse_args(argv)
if not args.interactive and not args.message:
parser.error("message is required unless --interactive is set")
return args
def main(argv: list[str] | None = None):
"""CLI main entry point."""
args = parse_args(argv)
api_key = args.api_key or os.environ.get("API_KEY") or os.environ.get("GLM_API_KEY")
base_url = args.base_url or os.environ.get("API_BASE", "https://api.z.ai/api/coding/paas/v4")
base_url = args.base_url or os.environ.get("API_BASE")
if not api_key:
print("Error: API key required. Set API_KEY env var or use --api-key", file=sys.stderr)

View file

@ -3,7 +3,7 @@ LangGraph-based agent factory for consistent tool calling.
"""
import os
from typing import Any, Callable, Optional
from typing import Any, Optional
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import BaseTool
@ -14,6 +14,7 @@ from langgraph.prebuilt import ToolNode
SYSTEM_PROMPT = """You are ZeroClaw, an AI assistant with tool access. Use tools to accomplish tasks.
Be concise and helpful. Execute tools directly when needed without excessive explanation."""
GLM_DEFAULT_BASE_URL = "https://api.z.ai/api/coding/paas/v4"
class ZeroclawAgent:
@ -40,7 +41,10 @@ class ZeroclawAgent:
self.system_prompt = system_prompt or SYSTEM_PROMPT
api_key = api_key or os.environ.get("API_KEY") or os.environ.get("GLM_API_KEY")
base_url = base_url or os.environ.get("API_BASE", "https://api.z.ai/api/coding/paas/v4")
base_url = base_url or os.environ.get("API_BASE")
if base_url is None and model.lower().startswith(("glm", "zhipu")):
base_url = GLM_DEFAULT_BASE_URL
if not api_key:
raise ValueError(
@ -105,7 +109,15 @@ class ZeroclawAgent:
"""
import asyncio
return asyncio.run(self.ainvoke(input, config))
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(self.ainvoke(input, config))
raise RuntimeError(
"ZeroclawAgent.invoke() cannot be called inside an active event loop. "
"Use 'await ZeroclawAgent.ainvoke(...)' instead."
)
def create_agent(

View file

@ -1,5 +1,5 @@
"""
Integrations for various platforms (Discord, Telegram, etc.)
Integrations for supported external platforms.
"""
from .discord_bot import DiscordBot

View file

@ -2,20 +2,18 @@
Discord bot integration for ZeroClaw.
"""
import asyncio
import os
from typing import Optional, Set
try:
import discord
from discord.ext import commands
DISCORD_AVAILABLE = True
except ImportError:
DISCORD_AVAILABLE = False
discord = None
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.messages import HumanMessage
from ..agent import create_agent
from ..tools import shell, file_read, file_write, web_search
@ -65,6 +63,18 @@ class DiscordBot:
self.model = model
self.prefix = prefix
if not self.api_key:
raise ValueError(
"API key required. Set API_KEY environment variable or pass api_key parameter."
)
self.agent = create_agent(
tools=[shell, file_read, file_write, web_search],
model=self.model,
api_key=self.api_key,
base_url=self.base_url,
)
self._histories: dict[str, list] = {}
self._max_history = 20
@ -117,13 +127,6 @@ class DiscordBot:
async def _process_message(self, content: str, user_id: str) -> str:
"""Process a message and return the response."""
agent = create_agent(
tools=[shell, file_read, file_write, web_search],
model=self.model,
api_key=self.api_key,
base_url=self.base_url,
)
messages = []
if user_id in self._histories:
@ -132,7 +135,7 @@ class DiscordBot:
messages.append(HumanMessage(content=content))
result = await agent.ainvoke({"messages": messages})
result = await self.agent.ainvoke({"messages": messages})
if user_id not in self._histories:
self._histories[user_id] = []

View file

@ -38,9 +38,13 @@ def tool(
```
"""
if func is not None:
return langchain_tool(func)
if name is not None:
return langchain_tool(name, func, description=description)
return langchain_tool(func, description=description)
def decorator(f: Callable) -> Any:
return langchain_tool(f, name=name)
if name is not None:
return langchain_tool(name, f, description=description)
return langchain_tool(f, description=description)
return decorator

View file

@ -3,7 +3,6 @@ Memory storage tools for persisting data between conversations.
"""
import json
import os
from pathlib import Path
from langchain_core.tools import tool
@ -20,7 +19,7 @@ def _load_memory() -> dict:
if not path.exists():
return {}
try:
with open(path, "r") as f:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
@ -30,7 +29,7 @@ def _save_memory(data: dict) -> None:
"""Save memory to disk."""
path = _get_memory_path()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)