From edf8ebe9a77db1191ad7e935e25577958e61e3af Mon Sep 17 00:00:00 2001 From: Youlu Date: Fri, 20 Feb 2026 00:00:15 -0800 Subject: [PATCH] auto: 2026-02-20 - update email processor, add ollama-local skill --- .clawhub/lock.json | 4 + logs/email_checks.log | 3 + scripts/email_processor/main.py | 4 +- skills/ollama-local/.clawhub/origin.json | 7 + skills/ollama-local/SKILL.md | 148 ++++++++++++ skills/ollama-local/_meta.json | 6 + skills/ollama-local/references/models.md | 105 +++++++++ skills/ollama-local/scripts/ollama.py | 232 +++++++++++++++++++ skills/ollama-local/scripts/ollama_tools.py | 237 ++++++++++++++++++++ 9 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 skills/ollama-local/.clawhub/origin.json create mode 100644 skills/ollama-local/SKILL.md create mode 100644 skills/ollama-local/_meta.json create mode 100644 skills/ollama-local/references/models.md create mode 100644 skills/ollama-local/scripts/ollama.py create mode 100644 skills/ollama-local/scripts/ollama_tools.py diff --git a/.clawhub/lock.json b/.clawhub/lock.json index 8bbec25..2325ee5 100644 --- a/.clawhub/lock.json +++ b/.clawhub/lock.json @@ -16,6 +16,10 @@ "agent-browser": { "version": "0.2.0", "installedAt": 1771517234848 + }, + "ollama-local": { + "version": "1.1.0", + "installedAt": 1771569100806 } } } diff --git a/logs/email_checks.log b/logs/email_checks.log index 3efddbf..e9b1fa3 100644 --- a/logs/email_checks.log +++ b/logs/email_checks.log @@ -18,3 +18,6 @@ Check: 2026-02-19 17:23:21 [2026-02-19T18:23:19-08:00] Heartbeat email check [2026-02-19T19:23:21-08:00] Heartbeat email check [2026-02-19T20:23:19-08:00] Heartbeat email check +[2026-02-19T22:23:19-08:00] Heartbeat email check +[2026-02-19T22:33:24-08:00] Heartbeat email check +[2026-02-19T23:33:27-08:00] Heartbeat email check diff --git a/scripts/email_processor/main.py b/scripts/email_processor/main.py index e714412..9589b15 100644 --- a/scripts/email_processor/main.py +++ b/scripts/email_processor/main.py @@ -86,7 +86,9 @@ def analyze_with_qwen3(email_data, config): import ollama import time - prompt = f"""Analyze this email and provide two pieces of information: + prompt = f"""/no_think + +Analyze this email and provide two pieces of information: 1. Is this an advertisement/promotional email? 2. Summarize the email in one sentence diff --git a/skills/ollama-local/.clawhub/origin.json b/skills/ollama-local/.clawhub/origin.json new file mode 100644 index 0000000..1b016e5 --- /dev/null +++ b/skills/ollama-local/.clawhub/origin.json @@ -0,0 +1,7 @@ +{ + "version": 1, + "registry": "https://clawhub.ai", + "slug": "ollama-local", + "installedVersion": "1.1.0", + "installedAt": 1771569100805 +} diff --git a/skills/ollama-local/SKILL.md b/skills/ollama-local/SKILL.md new file mode 100644 index 0000000..6c20952 --- /dev/null +++ b/skills/ollama-local/SKILL.md @@ -0,0 +1,148 @@ +--- +name: ollama-local +description: Manage and use local Ollama models. Use for model management (list/pull/remove), chat/completions, embeddings, and tool-use with local LLMs. Covers OpenClaw sub-agent integration and model selection guidance. +--- + +# Ollama Local + +Work with local Ollama models for inference, embeddings, and tool use. + +## Configuration + +Set your Ollama host (defaults to `http://localhost:11434`): + +```bash +export OLLAMA_HOST="http://localhost:11434" +# Or for remote server: +export OLLAMA_HOST="http://192.168.1.100:11434" +``` + +## Quick Reference + +```bash +# List models +python3 scripts/ollama.py list + +# Pull a model +python3 scripts/ollama.py pull llama3.1:8b + +# Remove a model +python3 scripts/ollama.py rm modelname + +# Show model details +python3 scripts/ollama.py show qwen3:4b + +# Chat with a model +python3 scripts/ollama.py chat qwen3:4b "What is the capital of France?" + +# Chat with system prompt +python3 scripts/ollama.py chat llama3.1:8b "Review this code" -s "You are a code reviewer" + +# Generate completion (non-chat) +python3 scripts/ollama.py generate qwen3:4b "Once upon a time" + +# Get embeddings +python3 scripts/ollama.py embed bge-m3 "Text to embed" +``` + +## Model Selection + +See [references/models.md](references/models.md) for full model list and selection guide. + +**Quick picks:** +- Fast answers: `qwen3:4b` +- Coding: `qwen2.5-coder:7b` +- General: `llama3.1:8b` +- Reasoning: `deepseek-r1:8b` + +## Tool Use + +Some local models support function calling. Use `ollama_tools.py`: + +```bash +# Single request with tools +python3 scripts/ollama_tools.py single qwen2.5-coder:7b "What's the weather in Amsterdam?" + +# Full tool loop (model calls tools, gets results, responds) +python3 scripts/ollama_tools.py loop qwen3:4b "Search for Python tutorials and summarize" + +# Show available example tools +python3 scripts/ollama_tools.py tools +``` + +**Tool-capable models:** qwen2.5-coder, qwen3, llama3.1, mistral + +## OpenClaw Sub-Agents + +Spawn local model sub-agents with `sessions_spawn`: + +```python +# Example: spawn a coding agent +sessions_spawn( + task="Review this Python code for bugs", + model="ollama/qwen2.5-coder:7b", + label="code-review" +) +``` + +Model path format: `ollama/` + +### Parallel Agents (Think Tank Pattern) + +Spawn multiple local agents for collaborative tasks: + +```python +agents = [ + {"label": "architect", "model": "ollama/gemma3:12b", "task": "Design the system architecture"}, + {"label": "coder", "model": "ollama/qwen2.5-coder:7b", "task": "Implement the core logic"}, + {"label": "reviewer", "model": "ollama/llama3.1:8b", "task": "Review for bugs and improvements"}, +] + +for a in agents: + sessions_spawn(task=a["task"], model=a["model"], label=a["label"]) +``` + +## Direct API + +For custom integrations, use the Ollama API directly: + +```bash +# Chat +curl $OLLAMA_HOST/api/chat -d '{ + "model": "qwen3:4b", + "messages": [{"role": "user", "content": "Hello"}], + "stream": false +}' + +# Generate +curl $OLLAMA_HOST/api/generate -d '{ + "model": "qwen3:4b", + "prompt": "Why is the sky blue?", + "stream": false +}' + +# List models +curl $OLLAMA_HOST/api/tags + +# Pull model +curl $OLLAMA_HOST/api/pull -d '{"name": "phi3:mini"}' +``` + +## Troubleshooting + +**Connection refused?** +- Check Ollama is running: `ollama serve` +- Verify OLLAMA_HOST is correct +- For remote servers, ensure firewall allows port 11434 + +**Model not loading?** +- Check VRAM: larger models may need CPU offload +- Try a smaller model first + +**Slow responses?** +- Model may be running on CPU +- Use smaller quantization (e.g., `:7b` instead of `:30b`) + +**OpenClaw sub-agent falls back to default model?** +- Ensure `ollama:default` auth profile exists in OpenClaw config +- Check model path format: `ollama/modelname:tag` diff --git a/skills/ollama-local/_meta.json b/skills/ollama-local/_meta.json new file mode 100644 index 0000000..2751475 --- /dev/null +++ b/skills/ollama-local/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7abphxkmfwh2z22jbs72ga7d80dyvd", + "slug": "ollama-local", + "version": "1.1.0", + "publishedAt": 1770105930012 +} \ No newline at end of file diff --git a/skills/ollama-local/references/models.md b/skills/ollama-local/references/models.md new file mode 100644 index 0000000..f2e8b0f --- /dev/null +++ b/skills/ollama-local/references/models.md @@ -0,0 +1,105 @@ +# Model Guide + +This reference covers common Ollama models and selection guidance. + +## Popular Models + +### Chat/General Models + +| Model | Params | Best For | Notes | +|-------|--------|----------|-------| +| `qwen3:4b` | 4B | Fast tasks, quick answers | Thinking-enabled, very fast | +| `llama3.1:8b` | 8B | General chat, reasoning | Good all-rounder | +| `gemma3:12b` | 12.2B | Creative, design tasks | Google model, good quality | +| `phi4-reasoning:latest` | 14.7B | Complex reasoning | Thinking-enabled | +| `mistral-small3.1:latest` | 24B | Technical tasks | May need CPU offload | +| `deepseek-r1:8b` | 8.2B | Deep reasoning | Thinking-enabled, chain-of-thought | + +### Coding Models + +| Model | Params | Best For | Notes | +|-------|--------|----------|-------| +| `qwen2.5-coder:7b` | 7.6B | Code generation, review | Best local coding model | +| `codellama:7b` | 7B | Code completion | Meta's code model | +| `deepseek-coder:6.7b` | 6.7B | Code tasks | Good alternative | + +### Embedding Models + +| Model | Params | Dimensions | Notes | +|-------|--------|------------|-------| +| `bge-m3:latest` | 567M | 1024 | Multilingual, good quality | +| `nomic-embed-text` | 137M | 768 | Fast, English-focused | +| `mxbai-embed-large` | 335M | 1024 | High quality embeddings | + +## Model Selection Guide + +### By Task Type + +- **Quick questions**: `qwen3:4b` (fastest) +- **General chat**: `llama3.1:8b` +- **Coding**: `qwen2.5-coder:7b` +- **Complex reasoning**: `phi4-reasoning` or `deepseek-r1:8b` +- **Creative/design**: `gemma3:12b` +- **Embeddings**: `bge-m3:latest` + +### By Speed vs Quality + +``` +Fastest ←──────────────────────────────→ Best Quality +qwen3:4b → llama3.1:8b → gemma3:12b → mistral-small3.1 +``` + +### Tool Use Support + +Models with good tool/function calling support: +- ✅ `qwen2.5-coder:7b` - Excellent +- ✅ `qwen3:4b` - Good +- ✅ `llama3.1:8b` - Basic +- ✅ `mistral` models - Good +- ⚠️ Others - May not support tools natively + +## OpenClaw Integration + +To use Ollama models in OpenClaw sub-agents, use these model paths: + +``` +ollama/qwen3:4b +ollama/llama3.1:8b +ollama/qwen2.5-coder:7b +ollama/gemma3:12b +ollama/mistral-small3.1:latest +ollama/phi4-reasoning:latest +ollama/deepseek-r1:8b +``` + +### Auth Profile Required + +OpenClaw requires an auth profile even for Ollama (no actual auth needed). Add to `auth-profiles.json`: + +```json +"ollama:default": { + "type": "api_key", + "provider": "ollama", + "key": "ollama" +} +``` + +## Hardware Considerations + +- **8GB VRAM**: Can run models up to ~13B comfortably +- **16GB VRAM**: Can run most models including 24B+ +- **CPU offload**: Ollama automatically offloads to CPU/RAM for larger models +- **Larger models** may be slower due to partial CPU inference + +## Installing Models + +```bash +# Pull a model +ollama pull llama3.1:8b + +# Or via the skill script +python3 scripts/ollama.py pull llama3.1:8b + +# List installed models +python3 scripts/ollama.py list +``` diff --git a/skills/ollama-local/scripts/ollama.py b/skills/ollama-local/scripts/ollama.py new file mode 100644 index 0000000..205b14d --- /dev/null +++ b/skills/ollama-local/scripts/ollama.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +""" +Ollama CLI helper for local Ollama servers. +Configure host via OLLAMA_HOST env var (default: http://localhost:11434) +""" + +import argparse +import json +import os +import sys +import urllib.request +import urllib.error + +OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434") + +def api_request(endpoint, method="GET", data=None): + """Make request to Ollama API (non-streaming).""" + url = f"{OLLAMA_HOST}{endpoint}" + headers = {"Content-Type": "application/json"} if data else {} + + req = urllib.request.Request( + url, + data=json.dumps(data).encode() if data else None, + headers=headers, + method=method + ) + + try: + with urllib.request.urlopen(req, timeout=300) as resp: + return json.loads(resp.read()) + except urllib.error.URLError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + +def api_stream(endpoint, data): + """Make streaming request to Ollama API.""" + url = f"{OLLAMA_HOST}{endpoint}" + + req = urllib.request.Request( + url, + data=json.dumps(data).encode(), + headers={"Content-Type": "application/json"}, + method="POST" + ) + + try: + with urllib.request.urlopen(req, timeout=300) as resp: + for line in resp: + if line.strip(): + yield json.loads(line) + except urllib.error.URLError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + +def list_models(): + """List all available models.""" + result = api_request("/api/tags") + models = result.get("models", []) + + if not models: + print("No models installed.") + return + + print(f"{'Model':<35} {'Size':<10} {'Params':<12} {'Family'}") + print("-" * 70) + for m in models: + name = m["name"] + size_gb = m["size"] / (1024**3) + params = m["details"].get("parameter_size", "?") + family = m["details"].get("family", "?") + print(f"{name:<35} {size_gb:>6.1f} GB {params:<12} {family}") + +def pull_model(model_name): + """Pull a model from Ollama registry.""" + print(f"Pulling {model_name}...") + + for chunk in api_stream("/api/pull", {"name": model_name}): + status = chunk.get("status", "") + if "pulling" in status: + completed = chunk.get("completed", 0) + total = chunk.get("total", 0) + if total: + pct = (completed / total) * 100 + print(f"\r{status}: {pct:.1f}%", end="", flush=True) + else: + print(f"\r{status}", end="", flush=True) + elif status: + print(f"\n{status}") + + print(f"\n✅ {model_name} pulled successfully") + +def remove_model(model_name): + """Remove a model.""" + api_request("/api/delete", method="DELETE", data={"name": model_name}) + print(f"✅ {model_name} removed") + +def show_model(model_name): + """Show model details.""" + result = api_request("/api/show", method="POST", data={"name": model_name}) + + print(f"Model: {model_name}") + print("-" * 40) + + if "details" in result: + d = result["details"] + print(f"Family: {d.get('family', '?')}") + print(f"Parameters: {d.get('parameter_size', '?')}") + print(f"Quantization: {d.get('quantization_level', '?')}") + + if "modelfile" in result: + print(f"\nModelfile:\n{result['modelfile'][:500]}...") + +def chat(model_name, message, system=None, stream=True): + """Chat with a model.""" + messages = [] + if system: + messages.append({"role": "system", "content": system}) + messages.append({"role": "user", "content": message}) + + data = { + "model": model_name, + "messages": messages, + "stream": stream + } + + if stream: + for chunk in api_stream("/api/chat", data): + content = chunk.get("message", {}).get("content", "") + if content: + print(content, end="", flush=True) + if chunk.get("done"): + print() # newline at end + # Print stats + if "eval_count" in chunk: + tokens = chunk["eval_count"] + duration = chunk.get("eval_duration", 0) / 1e9 + tps = tokens / duration if duration else 0 + print(f"\n[{tokens} tokens, {tps:.1f} tok/s]") + else: + data["stream"] = False + result = api_request("/api/chat", method="POST", data=data) + print(result.get("message", {}).get("content", "")) + +def generate(model_name, prompt, system=None, stream=True): + """Generate completion (non-chat mode).""" + data = { + "model": model_name, + "prompt": prompt, + "stream": stream + } + if system: + data["system"] = system + + if stream: + for chunk in api_stream("/api/generate", data): + response = chunk.get("response", "") + if response: + print(response, end="", flush=True) + if chunk.get("done"): + print() + else: + data["stream"] = False + result = api_request("/api/generate", method="POST", data=data) + print(result.get("response", "")) + +def embeddings(model_name, text): + """Get embeddings for text.""" + result = api_request("/api/embeddings", method="POST", data={ + "model": model_name, + "prompt": text + }) + emb = result.get("embedding", []) + print(f"Embedding dimensions: {len(emb)}") + print(f"First 10 values: {emb[:10]}") + +def main(): + parser = argparse.ArgumentParser(description="Ollama CLI helper") + subparsers = parser.add_subparsers(dest="command", required=True) + + # list + subparsers.add_parser("list", help="List all models") + + # pull + pull_parser = subparsers.add_parser("pull", help="Pull a model") + pull_parser.add_argument("model", help="Model name (e.g., llama3.1:8b)") + + # rm + rm_parser = subparsers.add_parser("rm", help="Remove a model") + rm_parser.add_argument("model", help="Model name") + + # show + show_parser = subparsers.add_parser("show", help="Show model details") + show_parser.add_argument("model", help="Model name") + + # chat + chat_parser = subparsers.add_parser("chat", help="Chat with a model") + chat_parser.add_argument("model", help="Model name") + chat_parser.add_argument("message", help="User message") + chat_parser.add_argument("-s", "--system", help="System prompt") + chat_parser.add_argument("--no-stream", action="store_true", help="Disable streaming") + + # generate + gen_parser = subparsers.add_parser("generate", help="Generate completion") + gen_parser.add_argument("model", help="Model name") + gen_parser.add_argument("prompt", help="Prompt text") + gen_parser.add_argument("-s", "--system", help="System prompt") + gen_parser.add_argument("--no-stream", action="store_true", help="Disable streaming") + + # embeddings + emb_parser = subparsers.add_parser("embed", help="Get embeddings") + emb_parser.add_argument("model", help="Model name (e.g., bge-m3)") + emb_parser.add_argument("text", help="Text to embed") + + args = parser.parse_args() + + if args.command == "list": + list_models() + elif args.command == "pull": + pull_model(args.model) + elif args.command == "rm": + remove_model(args.model) + elif args.command == "show": + show_model(args.model) + elif args.command == "chat": + chat(args.model, args.message, args.system, stream=not args.no_stream) + elif args.command == "generate": + generate(args.model, args.prompt, args.system, stream=not args.no_stream) + elif args.command == "embed": + embeddings(args.model, args.text) + +if __name__ == "__main__": + main() diff --git a/skills/ollama-local/scripts/ollama_tools.py b/skills/ollama-local/scripts/ollama_tools.py new file mode 100644 index 0000000..89b6a40 --- /dev/null +++ b/skills/ollama-local/scripts/ollama_tools.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +Ollama tool-use helper for local models. +Implements function calling pattern for models that support it. +Configure host via OLLAMA_HOST env var (default: http://localhost:11434) + +Note: Tool use support varies by model: +- qwen2.5-coder, qwen3: Good tool support +- llama3.1: Basic tool support +- mistral: Good tool support +- Others: May not support tools natively +""" + +import argparse +import json +import os +import sys +import urllib.request + +OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434") + +# Example tools for demonstration +EXAMPLE_TOOLS = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather for a location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City name, e.g., 'Amsterdam'" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "Temperature unit" + } + }, + "required": ["location"] + } + } + }, + { + "type": "function", + "function": { + "name": "search_web", + "description": "Search the web for information", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + } + }, + "required": ["query"] + } + } + }, + { + "type": "function", + "function": { + "name": "run_code", + "description": "Execute Python code and return the result", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Python code to execute" + } + }, + "required": ["code"] + } + } + } +] + +def chat_with_tools(model, message, tools=None, system=None): + """ + Send a chat request with tool definitions. + Returns the model's response, including any tool calls. + """ + tools = tools or EXAMPLE_TOOLS + + messages = [] + if system: + messages.append({"role": "system", "content": system}) + messages.append({"role": "user", "content": message}) + + data = { + "model": model, + "messages": messages, + "tools": tools, + "stream": False + } + + req = urllib.request.Request( + f"{OLLAMA_HOST}/api/chat", + data=json.dumps(data).encode(), + headers={"Content-Type": "application/json"}, + method="POST" + ) + + with urllib.request.urlopen(req, timeout=120) as resp: + result = json.loads(resp.read()) + + return result + +def execute_tool_call(tool_call): + """ + Execute a tool call (mock implementation). + In production, replace with actual implementations. + """ + name = tool_call.get("function", {}).get("name", "") + args = tool_call.get("function", {}).get("arguments", {}) + + # Parse arguments if string + if isinstance(args, str): + try: + args = json.loads(args) + except: + args = {} + + print(f" 🔧 Tool: {name}") + print(f" 📥 Args: {json.dumps(args, indent=2)}") + + # Mock responses + if name == "get_weather": + return {"temperature": 12, "condition": "cloudy", "location": args.get("location", "Unknown")} + elif name == "search_web": + return {"results": [f"Result for: {args.get('query', '')}"]} + elif name == "run_code": + return {"output": "Code execution simulated", "code": args.get("code", "")} + else: + return {"error": f"Unknown tool: {name}"} + +def tool_loop(model, message, max_iterations=5): + """ + Run a full tool-use loop: + 1. Send message with tools + 2. If model requests tool call, execute it + 3. Send tool result back + 4. Repeat until model gives final answer + """ + tools = EXAMPLE_TOOLS + messages = [{"role": "user", "content": message}] + + system = """You are a helpful assistant with access to tools. +When you need to use a tool, respond with a tool call. +After receiving tool results, provide a helpful answer to the user.""" + + for i in range(max_iterations): + print(f"\n--- Iteration {i+1} ---") + + data = { + "model": model, + "messages": [{"role": "system", "content": system}] + messages, + "tools": tools, + "stream": False + } + + req = urllib.request.Request( + f"{OLLAMA_HOST}/api/chat", + data=json.dumps(data).encode(), + headers={"Content-Type": "application/json"}, + method="POST" + ) + + with urllib.request.urlopen(req, timeout=120) as resp: + result = json.loads(resp.read()) + + msg = result.get("message", {}) + content = msg.get("content", "") + tool_calls = msg.get("tool_calls", []) + + if content: + print(f"🤖 Model: {content}") + + if not tool_calls: + print("\n✅ Final response (no more tool calls)") + return content + + # Process tool calls + messages.append({"role": "assistant", "content": content, "tool_calls": tool_calls}) + + for tc in tool_calls: + print(f"\n📞 Tool call requested:") + result = execute_tool_call(tc) + print(f" 📤 Result: {json.dumps(result)}") + + messages.append({ + "role": "tool", + "content": json.dumps(result) + }) + + print("\n⚠️ Max iterations reached") + return content + +def main(): + parser = argparse.ArgumentParser(description="Ollama tool-use helper") + subparsers = parser.add_subparsers(dest="command", required=True) + + # single: One-shot tool call + single_parser = subparsers.add_parser("single", help="Single tool-enabled request") + single_parser.add_argument("model", help="Model name") + single_parser.add_argument("message", help="User message") + + # loop: Full tool loop + loop_parser = subparsers.add_parser("loop", help="Full tool-use loop") + loop_parser.add_argument("model", help="Model name") + loop_parser.add_argument("message", help="User message") + loop_parser.add_argument("-n", "--max-iter", type=int, default=5, help="Max iterations") + + # tools: Show available tools + subparsers.add_parser("tools", help="Show example tools") + + args = parser.parse_args() + + if args.command == "single": + result = chat_with_tools(args.model, args.message) + msg = result.get("message", {}) + print(f"Content: {msg.get('content', '')}") + if msg.get("tool_calls"): + print(f"Tool calls: {json.dumps(msg['tool_calls'], indent=2)}") + + elif args.command == "loop": + tool_loop(args.model, args.message, args.max_iter) + + elif args.command == "tools": + print(json.dumps(EXAMPLE_TOOLS, indent=2)) + +if __name__ == "__main__": + main()