auto: 2026-02-20 - update email processor, add ollama-local skill

This commit is contained in:
2026-02-20 00:00:15 -08:00
parent 0b57216794
commit edf8ebe9a7
9 changed files with 745 additions and 1 deletions

View File

@@ -16,6 +16,10 @@
"agent-browser": {
"version": "0.2.0",
"installedAt": 1771517234848
},
"ollama-local": {
"version": "1.1.0",
"installedAt": 1771569100806
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "ollama-local",
"installedVersion": "1.1.0",
"installedAt": 1771569100805
}

View File

@@ -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/<model-name>`
### 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`

View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn7abphxkmfwh2z22jbs72ga7d80dyvd",
"slug": "ollama-local",
"version": "1.1.0",
"publishedAt": 1770105930012
}

View File

@@ -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
```

View File

@@ -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()

View File

@@ -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()