walidsobhie-code Claude Opus 4.6 commited on
Commit ·
b8e3e42
1
Parent(s): c7f1596
reorganize: consolidate root level to 20 folders
Browse files- Moved config files to /config/ (Dockerfile, pyproject, k8s, etc.)
- Moved docs to /docs/ (all markdown files)
- Organized scripts into /scripts/ (data, training, eval, deploy)
- Moved requirements to /requirements/
- Moved notebooks to /notebooks/
- Merged training configs and data into /training/
- Created selective archive in /archived-selective/ (300KB)
- All imports verified working
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This view is limited to 50 files because it contains too many changes. See raw diff
- TOOLS.md +0 -40
- archived-selective/BENCHMARK_BADGES.md +57 -0
- archived-selective/cli-backup/__init__.py +11 -0
- archived-selective/cli-backup/agent.py +660 -0
- archived-selective/cli-backup/cli.py +562 -0
- archived-selective/cli-backup/context.py +343 -0
- archived-selective/cli-backup/main.py +336 -0
- archived-selective/cli-backup/pyproject.toml +70 -0
- archived-selective/cli-backup/tools.py +1308 -0
- audit_report.txt → audits/audit_report.txt +0 -0
- audit_results.json → audits/audit_results.json +0 -0
- Dockerfile → config/Dockerfile +0 -0
- Dockerfile.gpu → config/Dockerfile.gpu +0 -0
- MLproject → config/MLproject +0 -0
- Makefile → config/Makefile +0 -0
- docker-compose.gpu.yml → config/docker-compose.gpu.yml +0 -0
- {k8s → config/k8s}/deployment.yaml +0 -0
- {k8s → config/k8s}/pvc.yaml +0 -0
- {k8s → config/k8s}/secret.yaml +0 -0
- {k8s → config/k8s}/service.yaml +0 -0
- package-lock.json → config/package-lock.json +0 -0
- package.json → config/package.json +0 -0
- pyproject.toml → config/pyproject.toml +0 -0
- tsconfig.json → config/tsconfig.json +0 -0
- CHANGELOG.md → docs/CHANGELOG.md +0 -0
- CODE_OF_CONDUCT.md → docs/CODE_OF_CONDUCT.md +0 -0
- CONTRIBUTING.md → docs/CONTRIBUTING.md +0 -0
- DIRECTORY_STRUCTURE.md → docs/DIRECTORY_STRUCTURE.md +0 -0
- GIT_PUSH.md → docs/GIT_PUSH.md +0 -0
- LAUNCH_CHECKLIST.md → docs/LAUNCH_CHECKLIST.md +0 -0
- LAUNCH_PLAN.md → docs/LAUNCH_PLAN.md +0 -0
- LICENSE → docs/LICENSE +0 -0
- MODEL_CARD.md → docs/MODEL_CARD.md +0 -0
- MODEL_REGISTRY.md → docs/MODEL_REGISTRY.md +0 -0
- README.md → docs/README.md +24 -0
- README_QUICKSTART.md → docs/README_QUICKSTART.md +0 -0
- SECURITY.md → docs/SECURITY.md +0 -0
- docs/tools.md +23 -145
- colab_train_stack29.ipynb → notebooks/colab_train_stack29.ipynb +0 -0
- kaggle_train_stack29_v5.ipynb → notebooks/kaggle_train_stack29_v5.ipynb +0 -0
- requirements.txt → requirements/requirements.txt +0 -0
- requirements_api.txt → requirements/requirements_api.txt +0 -0
- requirements_webui.txt → requirements/requirements_webui.txt +0 -0
- install.sh → scripts/deploy/install.sh +0 -0
- runpod_deploy.sh → scripts/deploy/runpod_deploy.sh +0 -0
- setup.sh → scripts/deploy/setup.sh +0 -0
- vastai_deploy.sh → scripts/deploy/vastai_deploy.sh +0 -0
- src/mcp_server.py +7 -0
- src/tools/grep_tool.py +12 -12
- stack/training/patterns/feedback.json +20 -0
TOOLS.md
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
# TOOLS.md - Local Notes
|
| 2 |
-
|
| 3 |
-
Skills define _how_ tools work. This file is for _your_ specifics — the stuff that's unique to your setup.
|
| 4 |
-
|
| 5 |
-
## What Goes Here
|
| 6 |
-
|
| 7 |
-
Things like:
|
| 8 |
-
|
| 9 |
-
- Camera names and locations
|
| 10 |
-
- SSH hosts and aliases
|
| 11 |
-
- Preferred voices for TTS
|
| 12 |
-
- Speaker/room names
|
| 13 |
-
- Device nicknames
|
| 14 |
-
- Anything environment-specific
|
| 15 |
-
|
| 16 |
-
## Examples
|
| 17 |
-
|
| 18 |
-
```markdown
|
| 19 |
-
### Cameras
|
| 20 |
-
|
| 21 |
-
- living-room → Main area, 180° wide angle
|
| 22 |
-
- front-door → Entrance, motion-triggered
|
| 23 |
-
|
| 24 |
-
### SSH
|
| 25 |
-
|
| 26 |
-
- home-server → 192.168.1.100, user: admin
|
| 27 |
-
|
| 28 |
-
### TTS
|
| 29 |
-
|
| 30 |
-
- Preferred voice: "Nova" (warm, slightly British)
|
| 31 |
-
- Default speaker: Kitchen HomePod
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
## Why Separate?
|
| 35 |
-
|
| 36 |
-
Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
|
| 37 |
-
|
| 38 |
-
---
|
| 39 |
-
|
| 40 |
-
Add whatever helps you do your job. This is your cheat sheet.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
archived-selective/BENCHMARK_BADGES.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Benchmark Badges for README.md
|
| 2 |
+
|
| 3 |
+
Copy these badge markdown lines into your main README to display evaluation scores.
|
| 4 |
+
|
| 5 |
+
## HumanEval Pass@k Badges
|
| 6 |
+
|
| 7 |
+
Replace the static badges with dynamic ones after evaluation completes.
|
| 8 |
+
|
| 9 |
+
### Template (placeholders):
|
| 10 |
+
|
| 11 |
+
```markdown
|
| 12 |
+
[](https://github.com/your-repo)
|
| 13 |
+
[](https://github.com/your-repo)
|
| 14 |
+
[](https://github.com/your-repo)
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
### Example with actual scores (update after running evaluation):
|
| 18 |
+
|
| 19 |
+
If you get 82% Pass@1:
|
| 20 |
+
```markdown
|
| 21 |
+
[](https://github.com/your-repo)
|
| 22 |
+
[](https://github.com/your-repo)
|
| 23 |
+
[](https://github.com/your-repo)
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## MBPP Badges
|
| 27 |
+
|
| 28 |
+
### Template:
|
| 29 |
+
```markdown
|
| 30 |
+
[](https://github.com/your-repo)
|
| 31 |
+
[](https://github.com/your-repo)
|
| 32 |
+
[](https://github.com/your-repo)
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Example with actual scores:
|
| 36 |
+
If you get 80% Pass@1:
|
| 37 |
+
```markdown
|
| 38 |
+
[](https://github.com/your-repo)
|
| 39 |
+
[](https://github.com/your-repo)
|
| 40 |
+
[](https://github.com/your-repo)
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Auto-generating Badges
|
| 44 |
+
|
| 45 |
+
After running evaluation, use the scores from the generated summary files:
|
| 46 |
+
|
| 47 |
+
- `results/humaneval_summary.json` → contains `pass@k` value for HumanEval
|
| 48 |
+
- `results/mbpp_summary.json` → contains `pass@k` value for MBPP
|
| 49 |
+
|
| 50 |
+
You can create a script to auto-update README.md with the latest scores.
|
| 51 |
+
|
| 52 |
+
## Combined Badges
|
| 53 |
+
|
| 54 |
+
```markdown
|
| 55 |
+
[](https://github.com/your-repo)
|
| 56 |
+
[](https://github.com/your-repo)
|
| 57 |
+
```
|
archived-selective/cli-backup/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stack 2.9 CLI Package
|
| 3 |
+
Terminal user interface for Stack 2.9
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
__version__ = "2.9.0"
|
| 7 |
+
__author__ = "my-ai-stack"
|
| 8 |
+
|
| 9 |
+
from .main import Stack29CLI, main
|
| 10 |
+
|
| 11 |
+
__all__ = ["Stack29CLI", "main"]
|
archived-selective/cli-backup/agent.py
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Stack 2.9 - Core Agent Logic Module
|
| 4 |
+
Query understanding, tool selection, response generation, and self-reflection loop.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import re
|
| 10 |
+
import asyncio
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Dict, List, Optional, Union, Callable
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
from enum import Enum
|
| 16 |
+
|
| 17 |
+
from .tools import TOOLS, get_tool, list_tools, get_tool_schemas
|
| 18 |
+
from .context import ContextManager, create_context_manager
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class QueryIntent(Enum):
|
| 22 |
+
"""Intents recognized by the agent."""
|
| 23 |
+
FILE_READ = "file_read"
|
| 24 |
+
FILE_WRITE = "file_write"
|
| 25 |
+
FILE_EDIT = "file_edit"
|
| 26 |
+
FILE_SEARCH = "file_search"
|
| 27 |
+
GIT_OPERATION = "git_operation"
|
| 28 |
+
CODE_EXECUTION = "code_execution"
|
| 29 |
+
WEB_SEARCH = "web_search"
|
| 30 |
+
MEMORY = "memory"
|
| 31 |
+
TASK = "task"
|
| 32 |
+
QUESTION = "question"
|
| 33 |
+
GENERAL = "general"
|
| 34 |
+
GENERAL_HELP = "general_help"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class ToolCall:
|
| 39 |
+
"""Represents a tool call."""
|
| 40 |
+
tool_name: str
|
| 41 |
+
arguments: Dict[str, Any]
|
| 42 |
+
result: Optional[Dict[str, Any]] = None
|
| 43 |
+
success: bool = False
|
| 44 |
+
error: Optional[str] = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass
|
| 48 |
+
class AgentResponse:
|
| 49 |
+
"""Represents the agent's response."""
|
| 50 |
+
content: str
|
| 51 |
+
tool_calls: List[ToolCall] = field(default_factory=list)
|
| 52 |
+
context_used: List[str] = field(default_factory=list)
|
| 53 |
+
confidence: float = 1.0
|
| 54 |
+
needs_clarification: bool = False
|
| 55 |
+
clarification_needed: Optional[str] = None
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class QueryUnderstanding:
|
| 59 |
+
"""Understands user queries and maps them to intents and tools."""
|
| 60 |
+
|
| 61 |
+
# Intent patterns
|
| 62 |
+
PATTERNS = {
|
| 63 |
+
QueryIntent.FILE_READ: [
|
| 64 |
+
r"read\s+(?:the\s+)?(?:file\s+)?(.+\.py|.+\.js|.+\.txt|.+\.md|.+\.json)",
|
| 65 |
+
r"show\s+(?:me\s+)?(?:the\s+)?(?:content\s+of\s+)?(.+\.py|.+\.js|.+\.txt|.+\.md|.+\.json)",
|
| 66 |
+
r"what('s| is)\s+in\s+(.+\.py|.+\.js|.+\.txt|.+\.md|.+\.json)",
|
| 67 |
+
r"cat\s+(.+)",
|
| 68 |
+
r"view\s+(.+)",
|
| 69 |
+
],
|
| 70 |
+
QueryIntent.FILE_WRITE: [
|
| 71 |
+
r"write\s+(?:to\s+)?(.+\.py|.+\.js|.+\.txt|.+\.md|.+\.json)",
|
| 72 |
+
r"create\s+(?:file\s+)?(.+\.py|.+\.js|.+\.txt|.+\.md|.+\.json)",
|
| 73 |
+
r"save\s+(?:to\s+)?(.+)",
|
| 74 |
+
],
|
| 75 |
+
QueryIntent.FILE_EDIT: [
|
| 76 |
+
r"edit\s+(.+\.py|.+\.js|.+\.txt|.+\.md|.+\.json)",
|
| 77 |
+
r"modify\s+(.+)",
|
| 78 |
+
r"change\s+(.+)",
|
| 79 |
+
r"replace\s+(.+)",
|
| 80 |
+
],
|
| 81 |
+
QueryIntent.FILE_SEARCH: [
|
| 82 |
+
r"find\s+(?:files?\s+)?(?:named\s+)?(.+)",
|
| 83 |
+
r"search\s+for\s+(?:files?\s+)?(.+)",
|
| 84 |
+
r"grep\s+for\s+(.+)",
|
| 85 |
+
r"where\s+is\s+(.+)",
|
| 86 |
+
r"locate\s+(.+)",
|
| 87 |
+
],
|
| 88 |
+
QueryIntent.GIT_OPERATION: [
|
| 89 |
+
r"git\s+(commit|push|pull|branch|status|log|diff)",
|
| 90 |
+
r"(commit|push|pull|branch)\s+(?:to\s+)?(?:the\s+)?(?:repo|repository)?",
|
| 91 |
+
],
|
| 92 |
+
QueryIntent.CODE_EXECUTION: [
|
| 93 |
+
r"^run\s+(?:the\s+)?(?:command\s+)?(.+)",
|
| 94 |
+
r"^execute\s+(.+)",
|
| 95 |
+
r"^start\s+(?:the\s+)?(?:server\s+)?(.+)",
|
| 96 |
+
r"^test\s+(?:the\s+)?(.+)",
|
| 97 |
+
r"^lint\s+(.+)",
|
| 98 |
+
r"^format\s+(.+)",
|
| 99 |
+
],
|
| 100 |
+
QueryIntent.WEB_SEARCH: [
|
| 101 |
+
r"^search\s+(?:the\s+)?web\s+for\s+(.+)",
|
| 102 |
+
r"^google\s+(.+)",
|
| 103 |
+
r"^look\s+up\s+(.+)",
|
| 104 |
+
r"^find\s+information\s+about\s+(.+)",
|
| 105 |
+
r"latest\s+ai\s+news",
|
| 106 |
+
r"what('s|\s+is)\s+new\s+in\s+ai",
|
| 107 |
+
],
|
| 108 |
+
QueryIntent.MEMORY: [
|
| 109 |
+
r"(remember|recall|what do you remember)\s+(.+)",
|
| 110 |
+
r"(save|store)\s+(?:to\s+)?memory\s+(.+)",
|
| 111 |
+
r"what('s| is)\s+in\s+(?:the\s+)?memory",
|
| 112 |
+
],
|
| 113 |
+
QueryIntent.GENERAL_HELP: [
|
| 114 |
+
r"list\s+(?:all\s+)?tools?",
|
| 115 |
+
r"what\s+tools\s+(?:do\s+you\s+have|can\s+you\s+do)",
|
| 116 |
+
r"help\s+me",
|
| 117 |
+
r"what\s+can\s+you\s+do",
|
| 118 |
+
r"how\s+to\s+use\s+you",
|
| 119 |
+
],
|
| 120 |
+
QueryIntent.TASK: [
|
| 121 |
+
r"(create|add|new)\s+task\s+(.+)",
|
| 122 |
+
r"list\s+(?:my\s+)?tasks?",
|
| 123 |
+
r"(complete|finish|done)\s+task\s+(.+)",
|
| 124 |
+
],
|
| 125 |
+
QueryIntent.QUESTION: [
|
| 126 |
+
r"what\s+is\s+(.+)",
|
| 127 |
+
r"how\s+(?:do|does)\s+(.+)",
|
| 128 |
+
r"why\s+(.+)",
|
| 129 |
+
r"can\s+(.+)",
|
| 130 |
+
r"(?:help|explain)\s+(.+)",
|
| 131 |
+
],
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
def __init__(self):
|
| 135 |
+
self.tools = list_tools()
|
| 136 |
+
|
| 137 |
+
def parse(self, query: str) -> Dict[str, Any]:
|
| 138 |
+
"""Parse query and determine intent."""
|
| 139 |
+
query = query.strip().lower()
|
| 140 |
+
|
| 141 |
+
# Check each intent pattern
|
| 142 |
+
for intent, patterns in self.PATTERNS.items():
|
| 143 |
+
for pattern in patterns:
|
| 144 |
+
match = re.search(pattern, query, re.IGNORECASE)
|
| 145 |
+
if match:
|
| 146 |
+
return {
|
| 147 |
+
"intent": intent.value,
|
| 148 |
+
"matched": match.group(0),
|
| 149 |
+
"extracted": match.groups() if match.groups() else None,
|
| 150 |
+
"confidence": 0.8
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
return {
|
| 154 |
+
"intent": QueryIntent.GENERAL.value,
|
| 155 |
+
"matched": None,
|
| 156 |
+
"extracted": None,
|
| 157 |
+
"confidence": 0.5
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
def extract_file_path(self, text: str) -> Optional[str]:
|
| 161 |
+
"""Extract file path from text."""
|
| 162 |
+
# Common patterns for file paths
|
| 163 |
+
patterns = [
|
| 164 |
+
r"([a-zA-Z0-9_/\-\.]+\.py)",
|
| 165 |
+
r"([a-zA-Z0-9_/\-\.]+\.js)",
|
| 166 |
+
r"([a-zA-Z0-9_/\-\.]+\.ts)",
|
| 167 |
+
r"([a-zA-Z0-9_/\-\.]+\.md)",
|
| 168 |
+
r"([a-zA-Z0-9_/\-\.]+\.json)",
|
| 169 |
+
r"([a-zA-Z0-9_/\-\.]+\.txt)",
|
| 170 |
+
r"([a-zA-Z0-9_/\-\.]+\.yaml|\.yml)",
|
| 171 |
+
r"([a-zA-Z0-9_/\-\.]+)",
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
for pattern in patterns:
|
| 175 |
+
match = re.search(pattern, text)
|
| 176 |
+
if match:
|
| 177 |
+
return match.group(1)
|
| 178 |
+
|
| 179 |
+
return None
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class ToolSelector:
|
| 183 |
+
"""Selects appropriate tools based on query intent."""
|
| 184 |
+
|
| 185 |
+
# Intent to tool mapping
|
| 186 |
+
INTENT_TOOLS = {
|
| 187 |
+
QueryIntent.FILE_READ: ["read"],
|
| 188 |
+
QueryIntent.FILE_WRITE: ["write"],
|
| 189 |
+
QueryIntent.FILE_EDIT: ["edit"],
|
| 190 |
+
QueryIntent.FILE_SEARCH: ["search", "grep"],
|
| 191 |
+
QueryIntent.GIT_OPERATION: ["git_status", "git_commit", "git_push", "git_pull", "git_branch", "git_log"],
|
| 192 |
+
QueryIntent.CODE_EXECUTION: ["run", "test", "lint", "format"],
|
| 193 |
+
QueryIntent.WEB_SEARCH: ["web_search", "fetch"],
|
| 194 |
+
QueryIntent.MEMORY: ["memory_recall", "memory_save", "memory_list"],
|
| 195 |
+
QueryIntent.TASK: ["create_task", "list_tasks", "update_task"],
|
| 196 |
+
QueryIntent.GENERAL_HELP: [],
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
def select(self, intent: str, context: Dict[str, Any]) -> List[str]:
|
| 200 |
+
"""Select tools for given intent."""
|
| 201 |
+
# Map string to QueryIntent enum
|
| 202 |
+
INTENT_MAP = {
|
| 203 |
+
"file_read": QueryIntent.FILE_READ,
|
| 204 |
+
"file_write": QueryIntent.FILE_WRITE,
|
| 205 |
+
"file_edit": QueryIntent.FILE_EDIT,
|
| 206 |
+
"file_search": QueryIntent.FILE_SEARCH,
|
| 207 |
+
"git_operation": QueryIntent.GIT_OPERATION,
|
| 208 |
+
"code_execution": QueryIntent.CODE_EXECUTION,
|
| 209 |
+
"web_search": QueryIntent.WEB_SEARCH,
|
| 210 |
+
"memory": QueryIntent.MEMORY,
|
| 211 |
+
"task": QueryIntent.TASK,
|
| 212 |
+
"general": QueryIntent.GENERAL,
|
| 213 |
+
"general_help": QueryIntent.GENERAL_HELP,
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
tools = []
|
| 217 |
+
intent_enum = INTENT_MAP.get(intent)
|
| 218 |
+
if intent_enum:
|
| 219 |
+
tools = list(self.INTENT_TOOLS.get(intent_enum, []))
|
| 220 |
+
|
| 221 |
+
# For git operations, filter based on query keywords
|
| 222 |
+
if intent == "git_operation" and context.get("query"):
|
| 223 |
+
query = context["query"].lower()
|
| 224 |
+
git_keyword_tools = {
|
| 225 |
+
"status": ["git_status"],
|
| 226 |
+
"commit": ["git_commit"],
|
| 227 |
+
"push": ["git_push"],
|
| 228 |
+
"pull": ["git_pull"],
|
| 229 |
+
"branch": ["git_branch"],
|
| 230 |
+
"log": ["git_log"],
|
| 231 |
+
"diff": ["git_diff"],
|
| 232 |
+
}
|
| 233 |
+
filtered = []
|
| 234 |
+
for kw, tool_list in git_keyword_tools.items():
|
| 235 |
+
if kw in query:
|
| 236 |
+
filtered.extend(tool_list)
|
| 237 |
+
# Default to git_status if no specific keyword found but query mentions git
|
| 238 |
+
if not filtered and "git" in query:
|
| 239 |
+
filtered = ["git_status"]
|
| 240 |
+
if filtered:
|
| 241 |
+
tools = filtered
|
| 242 |
+
|
| 243 |
+
return tools
|
| 244 |
+
|
| 245 |
+
def get_tool_parameters(self, tool_name: str, query: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
| 246 |
+
"""Extract parameters for a tool from query and context."""
|
| 247 |
+
params = {}
|
| 248 |
+
|
| 249 |
+
query_lower = query.lower()
|
| 250 |
+
|
| 251 |
+
if tool_name == "read":
|
| 252 |
+
path = re.search(r"(?:read|show|cat|view)\s+(?:the\s+)?(?:file\s+)?(.+)", query, re.IGNORECASE)
|
| 253 |
+
if path:
|
| 254 |
+
params["path"] = path.group(1).strip()
|
| 255 |
+
|
| 256 |
+
elif tool_name == "write":
|
| 257 |
+
path = re.search(r"write\s+(?:to\s+)?(.+?)(?:\s+with|\s+content|$)", query, re.IGNORECASE)
|
| 258 |
+
if path:
|
| 259 |
+
params["path"] = path.group(1).strip()
|
| 260 |
+
# Try to extract content
|
| 261 |
+
content_match = re.search(r"(?:content|with):\s*(.+)$", query, re.IGNORECASE)
|
| 262 |
+
if content_match:
|
| 263 |
+
params["content"] = content_match.group(1)
|
| 264 |
+
|
| 265 |
+
elif tool_name == "git_commit":
|
| 266 |
+
msg = re.search(r"commit(?:\s+with)?\s+(?:message\s+)?[\"']?(.+)[\"']?", query, re.IGNORECASE)
|
| 267 |
+
if msg:
|
| 268 |
+
params["message"] = msg.group(1).strip()
|
| 269 |
+
|
| 270 |
+
elif tool_name == "web_search":
|
| 271 |
+
# Extract search query
|
| 272 |
+
patterns = [
|
| 273 |
+
r"search\s+(?:the\s+)?web\s+for\s+(.+)",
|
| 274 |
+
r"google\s+(.+)",
|
| 275 |
+
r"look\s+up\s+(.+)",
|
| 276 |
+
r"latest\s+ai\s+news",
|
| 277 |
+
r"what('s|\s+is)\s+new\s+in\s+ai",
|
| 278 |
+
]
|
| 279 |
+
for pattern in patterns:
|
| 280 |
+
match = re.search(pattern, query, re.IGNORECASE)
|
| 281 |
+
if match:
|
| 282 |
+
# Only extract group(1) if it exists
|
| 283 |
+
if match.groups():
|
| 284 |
+
params["query"] = match.group(1).strip()
|
| 285 |
+
else:
|
| 286 |
+
# For patterns without capture groups, use full match
|
| 287 |
+
params["query"] = match.group(0).strip()
|
| 288 |
+
break
|
| 289 |
+
|
| 290 |
+
elif tool_name in ("grep", "search"):
|
| 291 |
+
# Extract pattern to search for - capture full phrase
|
| 292 |
+
# Strategy: split by " in " and take second part as path
|
| 293 |
+
parts = query.split(' in ')
|
| 294 |
+
if len(parts) >= 2:
|
| 295 |
+
# Last part is the path
|
| 296 |
+
path_part = ' in '.join(parts[1:])
|
| 297 |
+
# Clean up path
|
| 298 |
+
if path_part.strip() in ['project', 'this project']:
|
| 299 |
+
path_part = '/Users/walidsobhi/stack-2.9/src'
|
| 300 |
+
elif path_part.strip() == 'src':
|
| 301 |
+
path_part = '/Users/walidsobhi/stack-2.9/src'
|
| 302 |
+
elif path_part.startswith('~') or path_part.startswith('/') or path_part.startswith('./'):
|
| 303 |
+
pass # Keep as-is
|
| 304 |
+
else:
|
| 305 |
+
path_part = '/Users/walidsobhi/stack-2.9/' + path_part.strip()
|
| 306 |
+
params["path"] = path_part
|
| 307 |
+
# First part is the pattern (remove grep/for/search)
|
| 308 |
+
pattern_part = parts[0]
|
| 309 |
+
for prefix in ['grep for', 'search for', 'find for', 'grep', 'search', 'find']:
|
| 310 |
+
if pattern_part.strip().lower().startswith(prefix):
|
| 311 |
+
pattern_part = pattern_part.strip()[len(prefix):].strip()
|
| 312 |
+
break
|
| 313 |
+
params["pattern"] = pattern_part
|
| 314 |
+
else:
|
| 315 |
+
# Default path is workspace root
|
| 316 |
+
params["path"] = "/Users/walidsobhi/stack-2.9/src"
|
| 317 |
+
|
| 318 |
+
return params
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class ResponseGenerator:
|
| 322 |
+
"""Generates natural language responses."""
|
| 323 |
+
|
| 324 |
+
GREETING_VARIATIONS = [
|
| 325 |
+
"Sure! I can help with that.",
|
| 326 |
+
"Got it! Let me assist with that.",
|
| 327 |
+
"No problem! Here's what I found:",
|
| 328 |
+
"Alright! Here you go:",
|
| 329 |
+
"Sure thing! Let me show you:",
|
| 330 |
+
]
|
| 331 |
+
|
| 332 |
+
HELP_RESPONSES = [
|
| 333 |
+
"I support these operations:",
|
| 334 |
+
"Here are some things I can do:",
|
| 335 |
+
"Here's my toolkit:",
|
| 336 |
+
"I can help with the following:",
|
| 337 |
+
]
|
| 338 |
+
|
| 339 |
+
def __init__(self):
|
| 340 |
+
self.context_manager = create_context_manager()
|
| 341 |
+
self.last_intent = None
|
| 342 |
+
self.last_query = None
|
| 343 |
+
|
| 344 |
+
def generate(
|
| 345 |
+
self,
|
| 346 |
+
tool_results: List[ToolCall],
|
| 347 |
+
intent: str,
|
| 348 |
+
context: Dict[str, Any]
|
| 349 |
+
) -> str:
|
| 350 |
+
"""Generate response from tool results."""
|
| 351 |
+
import random
|
| 352 |
+
|
| 353 |
+
# Track intent for conversation flow
|
| 354 |
+
previous_intent = self.last_intent
|
| 355 |
+
self.last_intent = intent
|
| 356 |
+
|
| 357 |
+
if not tool_results:
|
| 358 |
+
# Handle queries that don't need tools
|
| 359 |
+
if intent == "question":
|
| 360 |
+
return ("I can help with reading/writing files, running commands, "
|
| 361 |
+
"git operations, web search, and more. "
|
| 362 |
+
"Try asking me something like 'read the file README.md' "
|
| 363 |
+
"or 'check git status'.")
|
| 364 |
+
elif intent == "general_help":
|
| 365 |
+
greeting = random.choice(self.HELP_RESPONSES)
|
| 366 |
+
return (f"{greeting}\n"
|
| 367 |
+
"- Read/write/edit files\n"
|
| 368 |
+
"- Run commands and code\n"
|
| 369 |
+
"- Git operations (status, commit, push, pull)\n"
|
| 370 |
+
"- Code search with grep\n"
|
| 371 |
+
"- Web search\n"
|
| 372 |
+
"- Manage tasks\n\n"
|
| 373 |
+
"Examples:\n"
|
| 374 |
+
"- 'read the file /path/to/file'\n"
|
| 375 |
+
"- 'check git status'\n"
|
| 376 |
+
"- 'grep for def main in ~/project/src'\n"
|
| 377 |
+
"- 'run the command ls'\n"
|
| 378 |
+
"- 'what is 2 + 2'")
|
| 379 |
+
elif intent == "general":
|
| 380 |
+
# Don't repeat the same greeting
|
| 381 |
+
if previous_intent == "general":
|
| 382 |
+
return "What would you like me to help you with?"
|
| 383 |
+
return "What can I help you with?"
|
| 384 |
+
return None
|
| 385 |
+
|
| 386 |
+
responses = []
|
| 387 |
+
greeting = random.choice(self.GREETING_VARIATIONS) if tool_results else None
|
| 388 |
+
|
| 389 |
+
for call in tool_results:
|
| 390 |
+
if call.result is None:
|
| 391 |
+
responses.append(f"Hmm, {call.tool_name} didn't return anything.")
|
| 392 |
+
continue
|
| 393 |
+
|
| 394 |
+
if call.result.get("success"):
|
| 395 |
+
result = call.result
|
| 396 |
+
|
| 397 |
+
# Format based on tool
|
| 398 |
+
if call.tool_name == "read":
|
| 399 |
+
if "content" in result:
|
| 400 |
+
content = result["content"]
|
| 401 |
+
if len(content) > 500:
|
| 402 |
+
content = content[:500] + "..."
|
| 403 |
+
responses.append(f"Here's the content:\n```\n{content}\n```")
|
| 404 |
+
|
| 405 |
+
elif call.tool_name == "search":
|
| 406 |
+
# Skip search tool if it has no matches - grep will show results
|
| 407 |
+
if "matches" in result and result["matches"]:
|
| 408 |
+
matches = result["matches"]
|
| 409 |
+
resp = f"Found {len(matches)} matches:\n"
|
| 410 |
+
for m in matches[:10]:
|
| 411 |
+
resp += f"- {m.get('file', '?')}:{m.get('line', '?')} - {m.get('content', '')}\n"
|
| 412 |
+
responses.append(resp)
|
| 413 |
+
# else: skip - grep will show results
|
| 414 |
+
|
| 415 |
+
elif call.tool_name == "grep":
|
| 416 |
+
if "matches" in result:
|
| 417 |
+
matches = result["matches"]
|
| 418 |
+
if matches:
|
| 419 |
+
resp = f"Found {len(matches)} matches:\n"
|
| 420 |
+
for m in matches[:10]:
|
| 421 |
+
resp += f"- {m.get('file', '?')}:{m.get('line', '?')} - {m.get('content', '')}\n"
|
| 422 |
+
responses.append(resp)
|
| 423 |
+
else:
|
| 424 |
+
responses.append("Didn't find any matches for that.")
|
| 425 |
+
|
| 426 |
+
elif call.tool_name in ["git_status", "git_log"]:
|
| 427 |
+
if "files" in result:
|
| 428 |
+
files = result["files"]
|
| 429 |
+
if files:
|
| 430 |
+
responses.append(f"Changed files ({len(files)}):\n" + "\n".join(f" - {f}" for f in files))
|
| 431 |
+
else:
|
| 432 |
+
responses.append("No changes detected.")
|
| 433 |
+
elif "commits" in result:
|
| 434 |
+
commits = result["commits"]
|
| 435 |
+
if commits:
|
| 436 |
+
responses.append("Recent commits:\n" + "\n".join(f" - {c}" for c in commits[:5]))
|
| 437 |
+
|
| 438 |
+
elif call.tool_name == "web_search":
|
| 439 |
+
if "results" in result:
|
| 440 |
+
results = result["results"]
|
| 441 |
+
resp = "Search results:\n"
|
| 442 |
+
for r in results[:5]:
|
| 443 |
+
resp += f"- {r.get('title', 'Untitled')}\n"
|
| 444 |
+
responses.append(resp)
|
| 445 |
+
|
| 446 |
+
elif call.tool_name == "run":
|
| 447 |
+
stdout = result.get("stdout", "")
|
| 448 |
+
stderr = result.get("stderr", "")
|
| 449 |
+
if stdout:
|
| 450 |
+
responses.append(f"Output:\n```\n{stdout[:500]}\n```")
|
| 451 |
+
if stderr:
|
| 452 |
+
responses.append(f"Errors:\n```\n{stderr[:500]}\n```")
|
| 453 |
+
if not stdout and not stderr:
|
| 454 |
+
responses.append("Command executed successfully.")
|
| 455 |
+
|
| 456 |
+
elif call.tool_name == "memory_recall":
|
| 457 |
+
if "matches" in result:
|
| 458 |
+
matches = result["matches"]
|
| 459 |
+
if matches:
|
| 460 |
+
responses.append(f"Found {len(matches)} memory entries.")
|
| 461 |
+
else:
|
| 462 |
+
responses.append("No matching memories found.")
|
| 463 |
+
|
| 464 |
+
else:
|
| 465 |
+
# Generic success response
|
| 466 |
+
responses.append(f"{call.tool_name}: {json.dumps(result)[:200]}")
|
| 467 |
+
else:
|
| 468 |
+
error = call.result.get("error", "Unknown error")
|
| 469 |
+
responses.append(f"Error in {call.tool_name}: {error}")
|
| 470 |
+
|
| 471 |
+
return "\n\n".join(responses) or "I processed your request but have no results to show."
|
| 472 |
+
|
| 473 |
+
def generate_clarification(self, question: str) -> str:
|
| 474 |
+
"""Generate clarification question."""
|
| 475 |
+
return f"I need some clarification: {question}"
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
class SelfReflection:
|
| 479 |
+
"""Self-reflection loop for improving responses."""
|
| 480 |
+
|
| 481 |
+
def __init__(self):
|
| 482 |
+
self.max_iterations = 3
|
| 483 |
+
self.min_confidence = 0.7
|
| 484 |
+
|
| 485 |
+
def reflect(
|
| 486 |
+
self,
|
| 487 |
+
query: str,
|
| 488 |
+
tool_calls: List[ToolCall],
|
| 489 |
+
response: str
|
| 490 |
+
) -> Dict[str, Any]:
|
| 491 |
+
"""Reflect on the response and determine if improvement is needed."""
|
| 492 |
+
# Check if any tool call failed
|
| 493 |
+
failed_calls = [c for c in tool_calls if not c.success]
|
| 494 |
+
|
| 495 |
+
# Calculate confidence
|
| 496 |
+
success_rate = len(tool_calls) / max(len(tool_calls), 1)
|
| 497 |
+
confidence = success_rate
|
| 498 |
+
|
| 499 |
+
needs_reflection = (
|
| 500 |
+
len(failed_calls) > 0 or
|
| 501 |
+
confidence < self.min_confidence or
|
| 502 |
+
len(response) < 20
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
return {
|
| 506 |
+
"needs_reflection": needs_reflection,
|
| 507 |
+
"confidence": confidence,
|
| 508 |
+
"failed_calls": len(failed_calls),
|
| 509 |
+
"response_length": len(response),
|
| 510 |
+
"suggestion": self._get_suggestion(failed_calls, confidence) if needs_reflection else None
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
def _get_suggestion(self, failed_calls: List[ToolCall], confidence: float) -> str:
|
| 514 |
+
"""Get improvement suggestion."""
|
| 515 |
+
if not failed_calls:
|
| 516 |
+
return "Try providing more context in your query."
|
| 517 |
+
|
| 518 |
+
return f"Failed tool calls: {', '.join(c.tool_name for c in failed_calls)}"
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
class StackAgent:
|
| 522 |
+
"""
|
| 523 |
+
Core agent that combines all components for intelligent assistance.
|
| 524 |
+
"""
|
| 525 |
+
|
| 526 |
+
def __init__(self, workspace: Optional[str] = None):
|
| 527 |
+
self.query_understanding = QueryUnderstanding()
|
| 528 |
+
self.tool_selector = ToolSelector()
|
| 529 |
+
self.response_generator = ResponseGenerator()
|
| 530 |
+
self.self_reflection = SelfReflection()
|
| 531 |
+
self.context_manager = create_context_manager(workspace)
|
| 532 |
+
self.conversation_history: List[Dict[str, Any]] = []
|
| 533 |
+
|
| 534 |
+
def process(self, query: str, context: Optional[Dict] = None) -> AgentResponse:
|
| 535 |
+
"""Process a user query."""
|
| 536 |
+
context = context or {}
|
| 537 |
+
|
| 538 |
+
# Step 1: Understand query
|
| 539 |
+
parsed = self.query_understanding.parse(query)
|
| 540 |
+
intent = parsed["intent"]
|
| 541 |
+
confidence = parsed["confidence"]
|
| 542 |
+
|
| 543 |
+
# Step 2: Select tools (pass query in context for smart filtering)
|
| 544 |
+
selected_tools = self.tool_selector.select(intent, {"query": query, **context})
|
| 545 |
+
tool_params = {}
|
| 546 |
+
|
| 547 |
+
for tool_name in selected_tools:
|
| 548 |
+
tool_params[tool_name] = self.tool_selector.get_tool_parameters(tool_name, query, context)
|
| 549 |
+
|
| 550 |
+
# Step 3: Execute tools
|
| 551 |
+
tool_calls = []
|
| 552 |
+
for tool_name in selected_tools:
|
| 553 |
+
tool = get_tool(tool_name)
|
| 554 |
+
if tool is None:
|
| 555 |
+
continue
|
| 556 |
+
|
| 557 |
+
params = tool_params.get(tool_name, {})
|
| 558 |
+
try:
|
| 559 |
+
result = tool(**params)
|
| 560 |
+
call = ToolCall(
|
| 561 |
+
tool_name=tool_name,
|
| 562 |
+
arguments=params,
|
| 563 |
+
result=result,
|
| 564 |
+
success=result.get("success", False) if isinstance(result, dict) else True
|
| 565 |
+
)
|
| 566 |
+
except Exception as e:
|
| 567 |
+
call = ToolCall(
|
| 568 |
+
tool_name=tool_name,
|
| 569 |
+
arguments=params,
|
| 570 |
+
error=str(e),
|
| 571 |
+
success=False
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
tool_calls.append(call)
|
| 575 |
+
|
| 576 |
+
# Record in session
|
| 577 |
+
self.context_manager.session.add_tool_usage(tool_name, call.result)
|
| 578 |
+
|
| 579 |
+
# Step 4: Generate response
|
| 580 |
+
response_content = self.response_generator.generate(tool_calls, intent, context)
|
| 581 |
+
|
| 582 |
+
# Step 5: Self-reflect
|
| 583 |
+
reflection = self.self_reflection.reflect(query, tool_calls, response_content)
|
| 584 |
+
|
| 585 |
+
# Step 6: Add to conversation history
|
| 586 |
+
self.conversation_history.append({
|
| 587 |
+
"query": query,
|
| 588 |
+
"intent": intent,
|
| 589 |
+
"tool_calls": [c.tool_name for c in tool_calls],
|
| 590 |
+
"response": response_content,
|
| 591 |
+
"reflection": reflection,
|
| 592 |
+
"timestamp": datetime.now().isoformat()
|
| 593 |
+
})
|
| 594 |
+
|
| 595 |
+
return AgentResponse(
|
| 596 |
+
content=response_content,
|
| 597 |
+
tool_calls=tool_calls,
|
| 598 |
+
confidence=reflection.get("confidence", confidence),
|
| 599 |
+
needs_clarification=reflection.get("needs_reflection", False),
|
| 600 |
+
clarification_needed=reflection.get("suggestion")
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
def process_with_tools(self, query: str, forced_tools: List[str]) -> AgentResponse:
|
| 604 |
+
"""Process query with explicitly specified tools."""
|
| 605 |
+
tool_calls = []
|
| 606 |
+
|
| 607 |
+
for tool_name in forced_tools:
|
| 608 |
+
tool = get_tool(tool_name)
|
| 609 |
+
if tool is None:
|
| 610 |
+
continue
|
| 611 |
+
|
| 612 |
+
try:
|
| 613 |
+
result = tool()
|
| 614 |
+
call = ToolCall(
|
| 615 |
+
tool_name=tool_name,
|
| 616 |
+
arguments={},
|
| 617 |
+
result=result,
|
| 618 |
+
success=result.get("success", False) if isinstance(result, dict) else True
|
| 619 |
+
)
|
| 620 |
+
except Exception as e:
|
| 621 |
+
call = ToolCall(
|
| 622 |
+
tool_name=tool_name,
|
| 623 |
+
arguments={},
|
| 624 |
+
error=str(e),
|
| 625 |
+
success=False
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
tool_calls.append(call)
|
| 629 |
+
|
| 630 |
+
response_content = self.response_generator.generate(tool_calls, "general", {})
|
| 631 |
+
|
| 632 |
+
return AgentResponse(
|
| 633 |
+
content=response_content,
|
| 634 |
+
tool_calls=tool_calls,
|
| 635 |
+
confidence=1.0
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
def get_context(self) -> str:
|
| 639 |
+
"""Get current context as string."""
|
| 640 |
+
return self.context_manager.get_workspace_context()
|
| 641 |
+
|
| 642 |
+
def get_schemas(self) -> List[Dict[str, Any]]:
|
| 643 |
+
"""Get tool schemas for tool calling."""
|
| 644 |
+
return get_tool_schemas()
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def create_agent(workspace: Optional[str] = None) -> StackAgent:
|
| 648 |
+
"""Factory function to create agent."""
|
| 649 |
+
return StackAgent(workspace)
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
if __name__ == "__main__":
|
| 653 |
+
print("Stack 2.9 Agent Module")
|
| 654 |
+
agent = create_agent()
|
| 655 |
+
print(f"Agent initialized with {len(list_tools())} tools")
|
| 656 |
+
|
| 657 |
+
# Test query
|
| 658 |
+
response = agent.process("list my tasks")
|
| 659 |
+
print(f"\nQuery: 'list my tasks'")
|
| 660 |
+
print(f"Response: {response.content[:200]}")
|
archived-selective/cli-backup/cli.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Stack 2.9 - CLI Entry Point
|
| 4 |
+
A complete CLI and agent interface showcasing Stack 2.9 capabilities.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import cmd
|
| 10 |
+
import json
|
| 11 |
+
import argparse
|
| 12 |
+
import asyncio
|
| 13 |
+
import subprocess
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any, Dict, List, Optional
|
| 16 |
+
|
| 17 |
+
from .agent import create_agent, StackAgent
|
| 18 |
+
from .context import create_context_manager
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# ============================================================================
|
| 22 |
+
# UTILITIES
|
| 23 |
+
# ============================================================================
|
| 24 |
+
|
| 25 |
+
def print_banner():
|
| 26 |
+
"""Print the Stack 2.9 banner."""
|
| 27 |
+
banner = r"""
|
| 28 |
+
____ _ _ _
|
| 29 |
+
| _ \ ___ _ __ __| |_ __ ___ (_)_ __ | | __
|
| 30 |
+
| |_) / _ \ '_ \ / _` | '__/ _ \| | '_ \ | |/ /
|
| 31 |
+
| _ < __/ | | | (_| | | | (_) | | | | | | <
|
| 32 |
+
|_| \_\___|_| |_|\__,_|_| \___/|_|_| |_| |_|\_\
|
| 33 |
+
|
| 34 |
+
Stack 2.9 CLI & Agent Interface
|
| 35 |
+
"""
|
| 36 |
+
print(banner)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def print_colored(text: str, color: str = "white", bold: bool = False):
|
| 40 |
+
"""Print colored text."""
|
| 41 |
+
colors = {
|
| 42 |
+
"red": "\033[91m",
|
| 43 |
+
"green": "\033[92m",
|
| 44 |
+
"yellow": "\033[93m",
|
| 45 |
+
"blue": "\033[94m",
|
| 46 |
+
"magenta": "\033[95m",
|
| 47 |
+
"cyan": "\033[96m",
|
| 48 |
+
"white": "\033[97m",
|
| 49 |
+
"reset": "\033[0m"
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if bold:
|
| 53 |
+
text = f"\033[1m{text}\033[0m"
|
| 54 |
+
|
| 55 |
+
sys.stdout.write(colors.get(color, "white"))
|
| 56 |
+
print(text)
|
| 57 |
+
sys.stdout.write(colors["reset"])
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def format_output(data: Any, format: str = "text") -> str:
|
| 61 |
+
"""Format output for display."""
|
| 62 |
+
if format == "json":
|
| 63 |
+
return json.dumps(data, indent=2)
|
| 64 |
+
elif isinstance(data, dict):
|
| 65 |
+
return "\n".join(f" {k}: {v}" for k, v in data.items())
|
| 66 |
+
elif isinstance(data, list):
|
| 67 |
+
return "\n".join(f" - {item}" for item in data)
|
| 68 |
+
else:
|
| 69 |
+
return str(data)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ============================================================================
|
| 73 |
+
# MODE 1: INTERACTIVE CHAT
|
| 74 |
+
# ============================================================================
|
| 75 |
+
|
| 76 |
+
class ChatMode(cmd.Cmd):
|
| 77 |
+
"""Interactive chat mode with the agent."""
|
| 78 |
+
|
| 79 |
+
intro = """
|
| 80 |
+
Welcome to Stack 2.9 Interactive Chat!
|
| 81 |
+
Type your queries or use commands:
|
| 82 |
+
/tools - List available tools
|
| 83 |
+
/schema - Show tool schemas
|
| 84 |
+
/context - Show current context
|
| 85 |
+
/history - Show conversation history
|
| 86 |
+
/clear - Clear chat history
|
| 87 |
+
/exit - Exit chat mode
|
| 88 |
+
|
| 89 |
+
Just type your question or task to get started!
|
| 90 |
+
"""
|
| 91 |
+
prompt = "\n[Stack]> "
|
| 92 |
+
|
| 93 |
+
def __init__(self, agent: StackAgent):
|
| 94 |
+
super().__init__()
|
| 95 |
+
self.agent = agent
|
| 96 |
+
self.history: List[Dict[str, Any]] = []
|
| 97 |
+
self.show_tool_calls = False
|
| 98 |
+
self.output_format = "text"
|
| 99 |
+
self.voice_input = False
|
| 100 |
+
|
| 101 |
+
def default(self, line: str):
|
| 102 |
+
"""Handle user input as a query to the agent."""
|
| 103 |
+
if line.startswith('/'):
|
| 104 |
+
return
|
| 105 |
+
|
| 106 |
+
print_colored("\nThinking...", "cyan")
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
response = self.agent.process(line)
|
| 110 |
+
self.history.append({
|
| 111 |
+
"query": line,
|
| 112 |
+
"response": response.content,
|
| 113 |
+
"tool_calls": [tc.tool_name for tc in response.tool_calls],
|
| 114 |
+
"timestamp": "now"
|
| 115 |
+
})
|
| 116 |
+
|
| 117 |
+
# Display response
|
| 118 |
+
print("\n" + "="*50)
|
| 119 |
+
print_colored("Response:", "green", bold=True)
|
| 120 |
+
print(response.content)
|
| 121 |
+
|
| 122 |
+
# Show tool usage if enabled
|
| 123 |
+
if self.show_tool_calls and response.tool_calls:
|
| 124 |
+
print("\n" + "-"*50)
|
| 125 |
+
print_colored("Tools Used:", "yellow")
|
| 126 |
+
for tc in response.tool_calls:
|
| 127 |
+
status = "✓" if tc.success else "✗"
|
| 128 |
+
print(f" {status} {tc.tool_name}")
|
| 129 |
+
if not tc.success and tc.error:
|
| 130 |
+
print(f" Error: {tc.error}")
|
| 131 |
+
|
| 132 |
+
if response.needs_clarification:
|
| 133 |
+
print_colored(f"\nNote: {response.clarification_needed}", "yellow")
|
| 134 |
+
|
| 135 |
+
except KeyboardInterrupt:
|
| 136 |
+
print_colored("\nInterrupted.", "red")
|
| 137 |
+
except Exception as e:
|
| 138 |
+
print_colored(f"\nError: {e}", "red")
|
| 139 |
+
|
| 140 |
+
def do_tools(self, arg):
|
| 141 |
+
"""List all available tools."""
|
| 142 |
+
tools = self.agent.context_manager.session.tools_used
|
| 143 |
+
if tools:
|
| 144 |
+
print(f"\nUsed {len(tools)} tools this session:")
|
| 145 |
+
for tool in set(tools):
|
| 146 |
+
print(f" - {tool}")
|
| 147 |
+
else:
|
| 148 |
+
print("\nNo tools used yet this session.")
|
| 149 |
+
|
| 150 |
+
def do_schema(self, arg):
|
| 151 |
+
"""Show tool schemas for tool calling."""
|
| 152 |
+
schemas = self.agent.get_schemas()
|
| 153 |
+
print(f"\nTool Schemas ({len(schemas)} tools):")
|
| 154 |
+
for schema in schemas[:10]:
|
| 155 |
+
print(f"\n {schema['name']}: {schema['description']}")
|
| 156 |
+
print(f"\n ... and {len(schemas) - 10} more")
|
| 157 |
+
|
| 158 |
+
def do_context(self, arg):
|
| 159 |
+
"""Show current context."""
|
| 160 |
+
context = self.agent.get_context()
|
| 161 |
+
print("\n" + context)
|
| 162 |
+
|
| 163 |
+
def do_history(self, arg):
|
| 164 |
+
"""Show conversation history."""
|
| 165 |
+
print(f"\nChat History ({len(self.history)} messages):")
|
| 166 |
+
for i, entry in enumerate(self.history[-10:], 1):
|
| 167 |
+
print(f"\n{i}. Query: {entry['query'][:50]}...")
|
| 168 |
+
print(f" Tools: {', '.join(entry['tool_calls'])}")
|
| 169 |
+
|
| 170 |
+
def do_clear(self, arg):
|
| 171 |
+
"""Clear chat history."""
|
| 172 |
+
self.history.clear()
|
| 173 |
+
self.agent.context_manager.session.messages.clear()
|
| 174 |
+
print_colored("Chat history cleared.", "green")
|
| 175 |
+
|
| 176 |
+
def do_voice(self, arg):
|
| 177 |
+
"""Toggle voice input."""
|
| 178 |
+
self.voice_input = not self.voice_input
|
| 179 |
+
status = "enabled" if self.voice_input else "disabled"
|
| 180 |
+
print_colored(f"Voice input {status} (note: requires additional setup)", "cyan")
|
| 181 |
+
|
| 182 |
+
def do_exit(self, arg):
|
| 183 |
+
"""Exit chat mode."""
|
| 184 |
+
print_colored("Goodbye!", "green")
|
| 185 |
+
return True
|
| 186 |
+
|
| 187 |
+
def do_EOF(self, arg):
|
| 188 |
+
"""Handle Ctrl+D."""
|
| 189 |
+
print()
|
| 190 |
+
return self.do_exit(arg)
|
| 191 |
+
|
| 192 |
+
def postcmd(self, stop: bool, line: str) -> bool:
|
| 193 |
+
"""Save session after each command."""
|
| 194 |
+
return stop
|
| 195 |
+
|
| 196 |
+
def run(self):
|
| 197 |
+
"""Run the chat loop."""
|
| 198 |
+
print_banner()
|
| 199 |
+
print(self.intro)
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
self.cmdloop()
|
| 203 |
+
except KeyboardInterrupt:
|
| 204 |
+
print("\nExiting...")
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ============================================================================
|
| 208 |
+
# MODE 2: COMMAND EXECUTION
|
| 209 |
+
# ============================================================================
|
| 210 |
+
|
| 211 |
+
class CommandMode:
|
| 212 |
+
"""Execute single commands."""
|
| 213 |
+
|
| 214 |
+
def __init__(self, agent: StackAgent):
|
| 215 |
+
self.agent = agent
|
| 216 |
+
self.context_manager = create_context_manager()
|
| 217 |
+
|
| 218 |
+
def execute(
|
| 219 |
+
self,
|
| 220 |
+
query: str,
|
| 221 |
+
output_file: Optional[str] = None,
|
| 222 |
+
output_format: str = "text"
|
| 223 |
+
) -> str:
|
| 224 |
+
"""Execute a query and return result."""
|
| 225 |
+
print(f"Executing: {query}")
|
| 226 |
+
|
| 227 |
+
response = self.agent.process(query)
|
| 228 |
+
|
| 229 |
+
result = format_output(response.content, output_format)
|
| 230 |
+
|
| 231 |
+
# Save to file if requested
|
| 232 |
+
if output_file:
|
| 233 |
+
with open(output_file, 'w') as f:
|
| 234 |
+
f.write(result)
|
| 235 |
+
print(f"Output saved to: {output_file}")
|
| 236 |
+
|
| 237 |
+
return result
|
| 238 |
+
|
| 239 |
+
def execute_tools(
|
| 240 |
+
self,
|
| 241 |
+
tools: List[str],
|
| 242 |
+
output_file: Optional[str] = None
|
| 243 |
+
) -> str:
|
| 244 |
+
"""Execute specific tools directly."""
|
| 245 |
+
print(f"Executing tools: {', '.join(tools)}")
|
| 246 |
+
|
| 247 |
+
response = self.agent.process_with_tools("", tools)
|
| 248 |
+
|
| 249 |
+
if output_file:
|
| 250 |
+
with open(output_file, 'w') as f:
|
| 251 |
+
f.write(response.content)
|
| 252 |
+
print(f"Output saved to: {output_file}")
|
| 253 |
+
|
| 254 |
+
return response.content
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# ============================================================================
|
| 258 |
+
# MODE 3: VOICE INTERFACE (PLACEHOLDER)
|
| 259 |
+
# ============================================================================
|
| 260 |
+
|
| 261 |
+
class VoiceInterface:
|
| 262 |
+
"""Voice input/output interface (requires additional setup)."""
|
| 263 |
+
|
| 264 |
+
def __init__(self):
|
| 265 |
+
self.available = self._check_availability()
|
| 266 |
+
|
| 267 |
+
def _check_availability(self) -> bool:
|
| 268 |
+
"""Check if voice dependencies are available."""
|
| 269 |
+
try:
|
| 270 |
+
import speech_recognition as sr
|
| 271 |
+
import pyttsx3
|
| 272 |
+
return True
|
| 273 |
+
except ImportError:
|
| 274 |
+
return False
|
| 275 |
+
|
| 276 |
+
def listen(self) -> Optional[str]:
|
| 277 |
+
"""Listen for voice input."""
|
| 278 |
+
if not self.available:
|
| 279 |
+
print("Voice recognition not available. Install with: pip install SpeechRecognition pyttsx3 pyaudio")
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
try:
|
| 283 |
+
import speech_recognition as sr
|
| 284 |
+
|
| 285 |
+
r = sr.Recognizer()
|
| 286 |
+
with sr.Microphone() as source:
|
| 287 |
+
print("Listening... (speak now)")
|
| 288 |
+
audio = r.listen(source, timeout=5)
|
| 289 |
+
|
| 290 |
+
text = r.recognize_google(audio)
|
| 291 |
+
print(f"Heard: {text}")
|
| 292 |
+
return text
|
| 293 |
+
except Exception as e:
|
| 294 |
+
print(f"Voice error: {e}")
|
| 295 |
+
return None
|
| 296 |
+
|
| 297 |
+
def speak(self, text: str):
|
| 298 |
+
"""Speak text output."""
|
| 299 |
+
if not self.available:
|
| 300 |
+
print(f"(TTS not available): {text}")
|
| 301 |
+
return
|
| 302 |
+
|
| 303 |
+
try:
|
| 304 |
+
import pyttsx3
|
| 305 |
+
|
| 306 |
+
engine = pyttsx3.init()
|
| 307 |
+
engine.say(text)
|
| 308 |
+
engine.runAndWait()
|
| 309 |
+
except Exception as e:
|
| 310 |
+
print(f"TTS error: {e}")
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
# ============================================================================
|
| 314 |
+
# MAIN CLI
|
| 315 |
+
# ============================================================================
|
| 316 |
+
|
| 317 |
+
class StackCLI:
|
| 318 |
+
"""Main CLI entry point."""
|
| 319 |
+
|
| 320 |
+
def __init__(self):
|
| 321 |
+
self.agent = create_agent()
|
| 322 |
+
self.chat_mode = ChatMode(self.agent)
|
| 323 |
+
self.command_mode = CommandMode(self.agent)
|
| 324 |
+
self.voice = VoiceInterface()
|
| 325 |
+
|
| 326 |
+
def run_interactive(self):
|
| 327 |
+
"""Run interactive chat mode."""
|
| 328 |
+
self.chat_mode.run()
|
| 329 |
+
|
| 330 |
+
def run_command(self, query: str, out_file: Optional[str] = None, fmt: str = "text"):
|
| 331 |
+
"""Run a single command."""
|
| 332 |
+
result = self.command_mode.execute(query, out_file, fmt)
|
| 333 |
+
print(result)
|
| 334 |
+
return result
|
| 335 |
+
|
| 336 |
+
def run_tools(self, tools: List[str], out_file: Optional[str] = None):
|
| 337 |
+
"""Run specific tools."""
|
| 338 |
+
result = self.command_mode.execute_tools(tools, out_file)
|
| 339 |
+
print(result)
|
| 340 |
+
return result
|
| 341 |
+
|
| 342 |
+
def run_eval(self, benchmark: str, provider: str = 'ollama', model: str = None):
|
| 343 |
+
"""Run evaluation benchmarks."""
|
| 344 |
+
print_colored(f"\n=== Running {benchmark} benchmark ===", "blue")
|
| 345 |
+
|
| 346 |
+
import sys
|
| 347 |
+
from pathlib import Path
|
| 348 |
+
eval_dir = Path(__file__).parent.parent / "stack-2.9-eval"
|
| 349 |
+
if eval_dir.exists():
|
| 350 |
+
sys.path.insert(0, str(eval_dir))
|
| 351 |
+
|
| 352 |
+
try:
|
| 353 |
+
if benchmark == 'mbpp':
|
| 354 |
+
from benchmarks.mbpp import MBPP
|
| 355 |
+
b = MBPP(model_provider=provider, model_name=model)
|
| 356 |
+
elif benchmark == 'human_eval':
|
| 357 |
+
from benchmarks.human_eval import HumanEval
|
| 358 |
+
b = HumanEval(model_provider=provider, model_name=model)
|
| 359 |
+
elif benchmark == 'gsm8k':
|
| 360 |
+
from benchmarks.gsm8k import GSM8K
|
| 361 |
+
b = GSM8K(model_provider=provider, model_name=model)
|
| 362 |
+
elif benchmark == 'all':
|
| 363 |
+
from benchmarks.mbpp import MBPP
|
| 364 |
+
from benchmarks.human_eval import HumanEval
|
| 365 |
+
from benchmarks.gsm8k import GSM8K
|
| 366 |
+
for name, Benchmark in [('MBPP', MBPP), ('HumanEval', HumanEval), ('GSM8K', GSM8K)]:
|
| 367 |
+
print_colored(f"\n--- {name} ---", "yellow")
|
| 368 |
+
b = Benchmark(model_provider=provider, model_name=model)
|
| 369 |
+
results = b.evaluate()
|
| 370 |
+
print(f" Accuracy: {results['accuracy']*100:.1f}%")
|
| 371 |
+
return
|
| 372 |
+
|
| 373 |
+
results = b.evaluate()
|
| 374 |
+
print_colored(f"\nResults:", "green")
|
| 375 |
+
print(f" Accuracy: {results['accuracy']*100:.1f}%")
|
| 376 |
+
print(f" Passed: {results['pass_at_1']}/{results['total_cases']}")
|
| 377 |
+
print(f" Model: {results['model']}")
|
| 378 |
+
except Exception as e:
|
| 379 |
+
print_colored(f"Error: {e}", "red")
|
| 380 |
+
|
| 381 |
+
def run_patterns(self, action: str):
|
| 382 |
+
"""Manage learned patterns."""
|
| 383 |
+
print_colored(f"\n=== Pattern Management ===", "blue")
|
| 384 |
+
|
| 385 |
+
import sys
|
| 386 |
+
from pathlib import Path
|
| 387 |
+
train_dir = Path(__file__).parent.parent / "stack-2.9-training"
|
| 388 |
+
if train_dir.exists():
|
| 389 |
+
sys.path.insert(0, str(train_dir))
|
| 390 |
+
|
| 391 |
+
try:
|
| 392 |
+
from pattern_miner import PatternMiner
|
| 393 |
+
miner = PatternMiner()
|
| 394 |
+
|
| 395 |
+
if action == 'list':
|
| 396 |
+
patterns = miner.get_relevant_patterns(limit=20)
|
| 397 |
+
print_colored(f"\nStored Patterns:", "yellow")
|
| 398 |
+
for p in patterns:
|
| 399 |
+
print(f" [{p.pattern_type}] {p.code_snippet[:50]}...")
|
| 400 |
+
elif action == 'stats':
|
| 401 |
+
stats = miner.get_statistics()
|
| 402 |
+
print_colored(f"\nStatistics:", "yellow")
|
| 403 |
+
print(f" Total Feedback: {stats['total_feedback']}")
|
| 404 |
+
print(f" Success Rate: {stats['success_rate']:.1%}")
|
| 405 |
+
print(f" Total Patterns: {stats['total_patterns']}")
|
| 406 |
+
except Exception as e:
|
| 407 |
+
print_colored(f"Error: {e}", "red")
|
| 408 |
+
|
| 409 |
+
def run_voice(self):
|
| 410 |
+
"""Run voice mode loop."""
|
| 411 |
+
if not self.voice.available:
|
| 412 |
+
print_colored("Voice interface not available.", "red")
|
| 413 |
+
return
|
| 414 |
+
|
| 415 |
+
print_banner()
|
| 416 |
+
print("Voice Mode - Say 'exit' to quit")
|
| 417 |
+
|
| 418 |
+
try:
|
| 419 |
+
while True:
|
| 420 |
+
text = self.voice.listen()
|
| 421 |
+
if not text:
|
| 422 |
+
continue
|
| 423 |
+
|
| 424 |
+
if "exit" in text.lower():
|
| 425 |
+
print("Exiting...")
|
| 426 |
+
break
|
| 427 |
+
|
| 428 |
+
# Process with agent
|
| 429 |
+
response = self.agent.process(text)
|
| 430 |
+
print(f"\nResponse: {response.content[:200]}")
|
| 431 |
+
|
| 432 |
+
# Speak response
|
| 433 |
+
self.voice.speak(response.content[:200])
|
| 434 |
+
|
| 435 |
+
except KeyboardInterrupt:
|
| 436 |
+
print("\nExiting...")
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def main():
|
| 440 |
+
"""Main entry point."""
|
| 441 |
+
parser = argparse.ArgumentParser(
|
| 442 |
+
description="Stack 2.9 CLI and Agent Interface",
|
| 443 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 444 |
+
epilog="""
|
| 445 |
+
Examples:
|
| 446 |
+
%(prog)s # Interactive chat mode
|
| 447 |
+
%(prog)s -c "read README.md" # Execute single command
|
| 448 |
+
%(prog)s -t read write # Execute specific tools
|
| 449 |
+
%(prog)s -v # Voice mode
|
| 450 |
+
"""
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
parser.add_argument(
|
| 454 |
+
'-c', '--command',
|
| 455 |
+
help="Execute a single query/command"
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
parser.add_argument(
|
| 459 |
+
'-t', '--tools',
|
| 460 |
+
nargs='+',
|
| 461 |
+
help="Execute specific tools"
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
parser.add_argument(
|
| 465 |
+
'-o', '--output',
|
| 466 |
+
help="Output file for results"
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
parser.add_argument(
|
| 470 |
+
'-f', '--format',
|
| 471 |
+
choices=['text', 'json'],
|
| 472 |
+
default='text',
|
| 473 |
+
help="Output format"
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
parser.add_argument(
|
| 477 |
+
'-v', '--voice',
|
| 478 |
+
action='store_true',
|
| 479 |
+
help="Enable voice mode"
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
parser.add_argument(
|
| 483 |
+
'-w', '--workspace',
|
| 484 |
+
default="/Users/walidsobhi/.openclaw/workspace",
|
| 485 |
+
help="Workspace path"
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
# Evaluation options
|
| 489 |
+
parser.add_argument(
|
| 490 |
+
'-e', '--eval',
|
| 491 |
+
choices=['mbpp', 'human_eval', 'gsm8k', 'all'],
|
| 492 |
+
help="Run evaluation benchmark"
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
parser.add_argument(
|
| 496 |
+
'--eval-provider',
|
| 497 |
+
default='ollama',
|
| 498 |
+
choices=['ollama', 'openai', 'anthropic', 'together'],
|
| 499 |
+
help="Model provider for evaluation"
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
parser.add_argument(
|
| 503 |
+
'--eval-model',
|
| 504 |
+
type=str,
|
| 505 |
+
help="Model name for evaluation"
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
# Pattern management
|
| 509 |
+
parser.add_argument(
|
| 510 |
+
'--patterns',
|
| 511 |
+
choices=['list', 'stats', 'clear'],
|
| 512 |
+
help="Manage learned patterns"
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
# Training
|
| 516 |
+
parser.add_argument(
|
| 517 |
+
'--train',
|
| 518 |
+
action='store_true',
|
| 519 |
+
help="Run LoRA training"
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
args = parser.parse_args()
|
| 523 |
+
|
| 524 |
+
try:
|
| 525 |
+
# Create CLI with custom workspace if provided
|
| 526 |
+
cli = StackCLI()
|
| 527 |
+
|
| 528 |
+
# Handle evaluation
|
| 529 |
+
if args.eval:
|
| 530 |
+
cli.run_eval(args.eval, args.eval_provider, args.eval_model)
|
| 531 |
+
return
|
| 532 |
+
|
| 533 |
+
# Handle pattern management
|
| 534 |
+
if args.patterns:
|
| 535 |
+
cli.run_patterns(args.patterns)
|
| 536 |
+
return
|
| 537 |
+
|
| 538 |
+
# Handle training
|
| 539 |
+
if args.train:
|
| 540 |
+
cli.run_train()
|
| 541 |
+
return
|
| 542 |
+
|
| 543 |
+
if args.voice:
|
| 544 |
+
cli.run_voice()
|
| 545 |
+
elif args.tools:
|
| 546 |
+
cli.run_tools(args.tools, args.output)
|
| 547 |
+
elif args.command:
|
| 548 |
+
exit(0 if cli.run_command(args.command, args.output, args.format) else 1)
|
| 549 |
+
else:
|
| 550 |
+
# Interactive mode
|
| 551 |
+
cli.run_interactive()
|
| 552 |
+
|
| 553 |
+
except KeyboardInterrupt:
|
| 554 |
+
print("\nGoodbye!")
|
| 555 |
+
sys.exit(0)
|
| 556 |
+
except Exception as e:
|
| 557 |
+
print(f"Error: {e}", file=sys.stderr)
|
| 558 |
+
sys.exit(1)
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
if __name__ == "__main__":
|
| 562 |
+
main()
|
archived-selective/cli-backup/context.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Stack 2.9 - Context Management Module
|
| 4 |
+
Handles project awareness, session memory, and long-term memory integration.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import re
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any, Dict, List, Optional, Set
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
from collections import defaultdict
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class ProjectContext:
|
| 19 |
+
"""Represents a project's context."""
|
| 20 |
+
name: str
|
| 21 |
+
path: str
|
| 22 |
+
language: Optional[str] = None
|
| 23 |
+
framework: Optional[str] = None
|
| 24 |
+
files: List[str] = field(default_factory=list)
|
| 25 |
+
dirs: List[str] = field(default_factory=list)
|
| 26 |
+
has_git: bool = False
|
| 27 |
+
dependencies: List[str] = field(default_factory=list)
|
| 28 |
+
entry_points: List[str] = field(default_factory=list)
|
| 29 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class SessionMemory:
|
| 34 |
+
"""Represents the current session's memory."""
|
| 35 |
+
messages: List[Dict[str, Any]] = field(default_factory=list)
|
| 36 |
+
tools_used: List[str] = field(default_factory=list)
|
| 37 |
+
files_touched: List[str] = field(default_factory=list)
|
| 38 |
+
commands_run: List[str] = field(default_factory=list)
|
| 39 |
+
created_at: datetime = field(default_factory=datetime.now)
|
| 40 |
+
last_updated: datetime = field(default_factory=datetime.now)
|
| 41 |
+
|
| 42 |
+
def add_message(self, role: str, content: str, metadata: Optional[Dict] = None):
|
| 43 |
+
"""Add a message to session memory."""
|
| 44 |
+
self.messages.append({
|
| 45 |
+
"role": role,
|
| 46 |
+
"content": content,
|
| 47 |
+
"timestamp": datetime.now().isoformat(),
|
| 48 |
+
"metadata": metadata or {}
|
| 49 |
+
})
|
| 50 |
+
self.last_updated = datetime.now()
|
| 51 |
+
|
| 52 |
+
def add_tool_usage(self, tool_name: str, result: Any):
|
| 53 |
+
"""Record tool usage."""
|
| 54 |
+
self.tools_used.append({
|
| 55 |
+
"tool": tool_name,
|
| 56 |
+
"timestamp": datetime.now().isoformat(),
|
| 57 |
+
"success": result.get("success", False) if isinstance(result, dict) else True
|
| 58 |
+
})
|
| 59 |
+
self.last_updated = datetime.now()
|
| 60 |
+
|
| 61 |
+
def add_file_touched(self, file_path: str, action: str):
|
| 62 |
+
"""Record file access."""
|
| 63 |
+
self.files_touched.append({
|
| 64 |
+
"path": file_path,
|
| 65 |
+
"action": action,
|
| 66 |
+
"timestamp": datetime.now().isoformat()
|
| 67 |
+
})
|
| 68 |
+
self.last_updated = datetime.now()
|
| 69 |
+
|
| 70 |
+
def add_command(self, command: str, result: Optional[Dict] = None):
|
| 71 |
+
"""Record command execution."""
|
| 72 |
+
self.commands_run.append({
|
| 73 |
+
"command": command,
|
| 74 |
+
"result": result,
|
| 75 |
+
"timestamp": datetime.now().isoformat()
|
| 76 |
+
})
|
| 77 |
+
self.last_updated = datetime.now()
|
| 78 |
+
|
| 79 |
+
def get_summary(self) -> Dict[str, Any]:
|
| 80 |
+
"""Get session summary."""
|
| 81 |
+
return {
|
| 82 |
+
"messages_count": len(self.messages),
|
| 83 |
+
"tools_used_count": len(self.tools_used),
|
| 84 |
+
"files_touched_count": len(self.files_touched),
|
| 85 |
+
"commands_run_count": len(self.commands_run),
|
| 86 |
+
"duration_minutes": (datetime.now() - self.created_at).total_seconds() / 60,
|
| 87 |
+
"created_at": self.created_at.isoformat(),
|
| 88 |
+
"last_updated": self.last_updated.isoformat()
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ContextManager:
|
| 93 |
+
"""Manages context across projects, sessions, and long-term memory."""
|
| 94 |
+
|
| 95 |
+
def __init__(self, workspace_path: str = "/Users/walidsobhi/.openclaw/workspace"):
|
| 96 |
+
self.workspace = Path(workspace_path)
|
| 97 |
+
self.session = SessionMemory()
|
| 98 |
+
self.projects: Dict[str, ProjectContext] = {}
|
| 99 |
+
self.current_project: Optional[ProjectContext] = None
|
| 100 |
+
self._load_context()
|
| 101 |
+
|
| 102 |
+
def _load_context(self):
|
| 103 |
+
"""Load existing context files."""
|
| 104 |
+
# Load workspace context files
|
| 105 |
+
context_files = {
|
| 106 |
+
"AGENTS.md": "agents",
|
| 107 |
+
"SOUL.md": "soul",
|
| 108 |
+
"TOOLS.md": "tools",
|
| 109 |
+
"USER.md": "user",
|
| 110 |
+
"MEMORY.md": "memory"
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
self.context = {}
|
| 114 |
+
for filename, key in context_files.items():
|
| 115 |
+
file_path = self.workspace / filename
|
| 116 |
+
if file_path.exists():
|
| 117 |
+
self.context[key] = file_path.read_text(encoding='utf-8')
|
| 118 |
+
|
| 119 |
+
# Scan for projects
|
| 120 |
+
self._scan_projects()
|
| 121 |
+
|
| 122 |
+
def _scan_projects(self):
|
| 123 |
+
"""Scan workspace for projects."""
|
| 124 |
+
for item in self.workspace.iterdir():
|
| 125 |
+
if item.is_dir() and not item.name.startswith('.'):
|
| 126 |
+
# Check if it's a project
|
| 127 |
+
if (item / "pyproject.toml").exists() or (item / "package.json").exists():
|
| 128 |
+
self.projects[item.name] = ProjectContext(
|
| 129 |
+
name=item.name,
|
| 130 |
+
path=str(item)
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
def load_project(self, project_name: str) -> Optional[ProjectContext]:
|
| 134 |
+
"""Load a specific project."""
|
| 135 |
+
project_path = self.workspace / project_name
|
| 136 |
+
|
| 137 |
+
if not project_path.exists():
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
# Create project context
|
| 141 |
+
ctx = ProjectContext(
|
| 142 |
+
name=project_name,
|
| 143 |
+
path=str(project_path)
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Detect language/framework
|
| 147 |
+
if (project_path / "pyproject.toml").exists():
|
| 148 |
+
ctx.language = "python"
|
| 149 |
+
try:
|
| 150 |
+
content = (project_path / "pyproject.toml").read_text()
|
| 151 |
+
if "fastapi" in content:
|
| 152 |
+
ctx.framework = "fastapi"
|
| 153 |
+
elif "django" in content:
|
| 154 |
+
ctx.framework = "django"
|
| 155 |
+
elif "flask" in content:
|
| 156 |
+
ctx.framework = "flask"
|
| 157 |
+
except:
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
if (project_path / "package.json").exists():
|
| 161 |
+
ctx.language = "javascript"
|
| 162 |
+
try:
|
| 163 |
+
content = json.loads((project_path / "package.json").read_text())
|
| 164 |
+
ctx.dependencies = list(content.get("dependencies", {}).keys())
|
| 165 |
+
if "next" in content.get("dependencies", {}):
|
| 166 |
+
ctx.framework = "next"
|
| 167 |
+
elif "react" in content.get("dependencies", {}):
|
| 168 |
+
ctx.framework = "react"
|
| 169 |
+
except:
|
| 170 |
+
pass
|
| 171 |
+
|
| 172 |
+
# Check for git
|
| 173 |
+
ctx.has_git = (project_path / ".git").exists()
|
| 174 |
+
|
| 175 |
+
# Scan files
|
| 176 |
+
try:
|
| 177 |
+
for item in project_path.rglob("*"):
|
| 178 |
+
if len(ctx.files) > 100:
|
| 179 |
+
break
|
| 180 |
+
rel = item.relative_to(project_path)
|
| 181 |
+
if item.is_file():
|
| 182 |
+
ctx.files.append(str(rel))
|
| 183 |
+
elif item.is_dir() and not item.name.startswith('.'):
|
| 184 |
+
ctx.dirs.append(str(rel))
|
| 185 |
+
except:
|
| 186 |
+
pass
|
| 187 |
+
|
| 188 |
+
# Find entry points
|
| 189 |
+
entry_patterns = ["main.py", "app.py", "index.js", "main.js", "server.py"]
|
| 190 |
+
for pattern in entry_patterns:
|
| 191 |
+
for f in ctx.files:
|
| 192 |
+
if f.endswith(pattern):
|
| 193 |
+
ctx.entry_points.append(f)
|
| 194 |
+
|
| 195 |
+
self.projects[project_name] = ctx
|
| 196 |
+
self.current_project = ctx
|
| 197 |
+
return ctx
|
| 198 |
+
|
| 199 |
+
def get_context_summary(self) -> Dict[str, Any]:
|
| 200 |
+
"""Get context summary."""
|
| 201 |
+
return {
|
| 202 |
+
"workspace": str(self.workspace),
|
| 203 |
+
"projects": list(self.projects.keys()),
|
| 204 |
+
"current_project": self.current_project.name if self.current_project else None,
|
| 205 |
+
"session": self.session.get_summary(),
|
| 206 |
+
"has_agents": "agents" in self.context,
|
| 207 |
+
"has_soul": "soul" in self.context,
|
| 208 |
+
"has_tools": "tools" in self.context,
|
| 209 |
+
"has_memory": "memory" in self.context
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
def get_workspace_context(self) -> str:
|
| 213 |
+
"""Get formatted workspace context."""
|
| 214 |
+
lines = ["# Workspace Context"]
|
| 215 |
+
lines.append(f"\n## Projects ({len(self.projects)})")
|
| 216 |
+
|
| 217 |
+
for name, proj in self.projects.items():
|
| 218 |
+
lines.append(f"- **{name}** ({proj.language or 'unknown'})")
|
| 219 |
+
if proj.framework:
|
| 220 |
+
lines.append(f" - Framework: {proj.framework}")
|
| 221 |
+
lines.append(f" - Path: {proj.path}")
|
| 222 |
+
if proj.has_git:
|
| 223 |
+
lines.append(" - Git: ✓")
|
| 224 |
+
|
| 225 |
+
if self.current_project:
|
| 226 |
+
lines.append(f"\n## Current Project: {self.current_project.name}")
|
| 227 |
+
lines.append(f"- Files: {len(self.current_project.files)}")
|
| 228 |
+
lines.append(f"- Dirs: {len(self.current_project.dirs)}")
|
| 229 |
+
if self.current_project.entry_points:
|
| 230 |
+
lines.append(f"- Entry: {self.current_project.entry_points[0]}")
|
| 231 |
+
|
| 232 |
+
lines.append(f"\n## Session")
|
| 233 |
+
summary = self.session.get_summary()
|
| 234 |
+
lines.append(f"- Messages: {summary['messages_count']}")
|
| 235 |
+
lines.append(f"- Tools used: {summary['tools_used_count']}")
|
| 236 |
+
lines.append(f"- Files touched: {summary['files_touched_count']}")
|
| 237 |
+
|
| 238 |
+
return "\n".join(lines)
|
| 239 |
+
|
| 240 |
+
def search_memory(self, query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
| 241 |
+
"""Search long-term memory."""
|
| 242 |
+
results = []
|
| 243 |
+
|
| 244 |
+
# Search MEMORY.md
|
| 245 |
+
memory_file = self.workspace / "MEMORY.md"
|
| 246 |
+
if memory_file.exists():
|
| 247 |
+
content = memory_file.read_text()
|
| 248 |
+
if query.lower() in content.lower():
|
| 249 |
+
results.append({
|
| 250 |
+
"file": str(memory_file),
|
| 251 |
+
"type": "memory",
|
| 252 |
+
"content": content[:500]
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
# Search memory folder
|
| 256 |
+
memory_dir = self.workspace / "memory"
|
| 257 |
+
if memory_dir.exists():
|
| 258 |
+
for f in memory_dir.rglob("*.md"):
|
| 259 |
+
try:
|
| 260 |
+
content = f.read_text()
|
| 261 |
+
if query.lower() in content.lower():
|
| 262 |
+
results.append({
|
| 263 |
+
"file": str(f),
|
| 264 |
+
"type": "daily",
|
| 265 |
+
"content": content[:500]
|
| 266 |
+
})
|
| 267 |
+
except:
|
| 268 |
+
continue
|
| 269 |
+
|
| 270 |
+
return results[:max_results]
|
| 271 |
+
|
| 272 |
+
def save_to_memory(self, key: str, value: str):
|
| 273 |
+
"""Save to long-term memory."""
|
| 274 |
+
memory_file = self.workspace / "MEMORY.md"
|
| 275 |
+
|
| 276 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 277 |
+
entry = f"\n### {key}\n_{timestamp}_\n{value}\n"
|
| 278 |
+
|
| 279 |
+
with open(memory_file, "a") as f:
|
| 280 |
+
f.write(entry)
|
| 281 |
+
|
| 282 |
+
def get_recent_context(self, days: int = 7) -> List[Dict[str, Any]]:
|
| 283 |
+
"""Get recent context from memory."""
|
| 284 |
+
results = []
|
| 285 |
+
|
| 286 |
+
memory_dir = self.workspace / "memory"
|
| 287 |
+
if memory_dir.exists():
|
| 288 |
+
# Get files from last N days
|
| 289 |
+
cutoff = datetime.now() - timedelta(days=days)
|
| 290 |
+
|
| 291 |
+
for f in sorted(memory_dir.glob("*.md"), key=lambda x: x.stat().st_mtime, reverse=True):
|
| 292 |
+
try:
|
| 293 |
+
mtime = datetime.fromtimestamp(f.stat().st_mtime)
|
| 294 |
+
if mtime > cutoff:
|
| 295 |
+
content = f.read_text()
|
| 296 |
+
results.append({
|
| 297 |
+
"file": str(f),
|
| 298 |
+
"date": mtime.isoformat(),
|
| 299 |
+
"content": content[:1000]
|
| 300 |
+
})
|
| 301 |
+
except:
|
| 302 |
+
continue
|
| 303 |
+
|
| 304 |
+
return results
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
class ProjectAware:
|
| 308 |
+
"""Mixin for project-aware functionality."""
|
| 309 |
+
|
| 310 |
+
def __init__(self):
|
| 311 |
+
self.context_manager = ContextManager()
|
| 312 |
+
|
| 313 |
+
def detect_project(self, path: str) -> Optional[str]:
|
| 314 |
+
"""Detect project from path."""
|
| 315 |
+
p = Path(path).resolve()
|
| 316 |
+
|
| 317 |
+
# Walk up to find project root
|
| 318 |
+
while p != p.parent:
|
| 319 |
+
for name in ["pyproject.toml", "package.json", "Cargo.toml", "go.mod"]:
|
| 320 |
+
if (p / name).exists():
|
| 321 |
+
return p.name
|
| 322 |
+
p = p.parent
|
| 323 |
+
|
| 324 |
+
return None
|
| 325 |
+
|
| 326 |
+
def get_project_context(self, project_name: str) -> Optional[ProjectContext]:
|
| 327 |
+
"""Get project context."""
|
| 328 |
+
return self.context_manager.load_project(project_name)
|
| 329 |
+
|
| 330 |
+
def format_context_for_prompt(self) -> str:
|
| 331 |
+
"""Format context for LLM prompt."""
|
| 332 |
+
return self.context_manager.get_workspace_context()
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def create_context_manager(workspace: Optional[str] = None) -> ContextManager:
|
| 336 |
+
"""Factory function to create context manager."""
|
| 337 |
+
return ContextManager(workspace or "/Users/walidsobhi/.openclaw/workspace")
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
if __name__ == "__main__":
|
| 341 |
+
print("Stack 2.9 Context Module")
|
| 342 |
+
cm = ContextManager()
|
| 343 |
+
print(cm.get_context_summary())
|
archived-selective/cli-backup/main.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Stack 2.9 CLI - Terminal User Interface
|
| 4 |
+
Main entry point for interacting with Stack 2.9
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import argparse
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
# Add parent directories to path
|
| 14 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).parent.parent / "stack" / "eval"))
|
| 16 |
+
sys.path.insert(0, str(Path(__file__).parent.parent / "stack" / "training"))
|
| 17 |
+
|
| 18 |
+
from model_client import create_model_client, ChatMessage
|
| 19 |
+
from pattern_miner import PatternMiner
|
| 20 |
+
from data_quality import DataQualityAnalyzer
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Stack29CLI:
|
| 24 |
+
"""Stack 2.9 Terminal User Interface"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, provider: str = None, model: str = None):
|
| 27 |
+
self.provider = provider or os.environ.get("MODEL_PROVIDER", "ollama")
|
| 28 |
+
self.model = model or os.environ.get("MODEL_NAME", "")
|
| 29 |
+
self.client = None
|
| 30 |
+
self.agent = None
|
| 31 |
+
self.miner = PatternMiner()
|
| 32 |
+
self.chat_history = []
|
| 33 |
+
|
| 34 |
+
# Colors
|
| 35 |
+
self.BLUE = '\033[94m'
|
| 36 |
+
self.GREEN = '\033[92m'
|
| 37 |
+
self.YELLOW = '\033[93m'
|
| 38 |
+
self.RED = '\033[91m'
|
| 39 |
+
self.END = '\033[0m'
|
| 40 |
+
self.BOLD = '\033[1m'
|
| 41 |
+
|
| 42 |
+
def print_header(self):
|
| 43 |
+
"""Print CLI header"""
|
| 44 |
+
print(f"""
|
| 45 |
+
{self.BLUE}╔═══════════════════════════════════════════════════════╗
|
| 46 |
+
║ {self.BOLD}Stack 2.9 - Self-Evolving AI{self.END}{self.BLUE} ║
|
| 47 |
+
║ {self.YELLOW}Your AI coding companion{self.END}{self.BLUE} ║
|
| 48 |
+
╚═══════════════════════════════════════════════════════╝{self.END}
|
| 49 |
+
""")
|
| 50 |
+
|
| 51 |
+
def print_menu(self):
|
| 52 |
+
"""Print main menu"""
|
| 53 |
+
print(f"""
|
| 54 |
+
{self.BOLD}Main Menu:{self.END}
|
| 55 |
+
{self.GREEN}[1]{self.END} Chat with Stack 2.9
|
| 56 |
+
{self.GREEN}[2]{self.END} Run Evaluation (Benchmarks)
|
| 57 |
+
{self.GREEN}[3]{self.END} Manage Patterns (Self-Evolution)
|
| 58 |
+
{self.GREEN}[4]{self.END} Train Model (LoRA Fine-tuning)
|
| 59 |
+
{self.GREEN}[5]{self.END} Settings
|
| 60 |
+
{self.GREEN}[0]{self.END} Exit
|
| 61 |
+
|
| 62 |
+
""")
|
| 63 |
+
|
| 64 |
+
def init_client(self):
|
| 65 |
+
"""Initialize model client"""
|
| 66 |
+
try:
|
| 67 |
+
self.client = create_model_client(self.provider, self.model)
|
| 68 |
+
print(f"{self.GREEN}✓{self.END} Connected to {self.provider}: {self.client.get_model_name()}")
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"{self.RED}✗{self.END} Failed to connect: {e}")
|
| 71 |
+
print(f"{self.YELLOW}!{self.END} Make sure {self.provider} is running")
|
| 72 |
+
self.client = None
|
| 73 |
+
|
| 74 |
+
def chat_mode(self):
|
| 75 |
+
"""Interactive chat mode using agent with tool calling"""
|
| 76 |
+
if not self.client:
|
| 77 |
+
print(f"{self.RED}No model connected!{self.END}")
|
| 78 |
+
return
|
| 79 |
+
|
| 80 |
+
print(f"\n{self.BLUE}=== Chat Mode ==={self.END}")
|
| 81 |
+
print("Type 'exit' to return to menu, 'clear' to clear history\n")
|
| 82 |
+
|
| 83 |
+
# Initialize agent if not done
|
| 84 |
+
if not hasattr(self, 'agent') or self.agent is None:
|
| 85 |
+
from cli.agent import StackAgent
|
| 86 |
+
self.agent = StackAgent(workspace='/Users/walidsobhi/stack-2.9')
|
| 87 |
+
print(f"{self.GREEN}✓{self.END} Agent initialized")
|
| 88 |
+
|
| 89 |
+
while True:
|
| 90 |
+
try:
|
| 91 |
+
user_input = input(f"{self.GREEN}You:{self.END} ").strip()
|
| 92 |
+
|
| 93 |
+
if not user_input:
|
| 94 |
+
continue
|
| 95 |
+
|
| 96 |
+
if user_input.lower() in ['exit', 'quit', 'q']:
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
if user_input.lower() == 'clear':
|
| 100 |
+
print("Chat cleared.\n")
|
| 101 |
+
continue
|
| 102 |
+
|
| 103 |
+
# Process through agent (handles tool calling)
|
| 104 |
+
print(f"{self.BLUE}Stack 2.9:{self.END} ", end="", flush=True)
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
response = self.agent.process(user_input)
|
| 108 |
+
print(response.content)
|
| 109 |
+
|
| 110 |
+
except Exception as e:
|
| 111 |
+
print(f"{self.RED}Error: {e}{self.END}")
|
| 112 |
+
|
| 113 |
+
except Exception as e:
|
| 114 |
+
print(f"{self.RED}Error: {e}{self.END}")
|
| 115 |
+
|
| 116 |
+
print()
|
| 117 |
+
|
| 118 |
+
except KeyboardInterrupt:
|
| 119 |
+
print("\n")
|
| 120 |
+
break
|
| 121 |
+
|
| 122 |
+
def eval_mode(self):
|
| 123 |
+
"""Run evaluation benchmarks"""
|
| 124 |
+
print(f"\n{self.BLUE}=== Evaluation ==={self.END}")
|
| 125 |
+
print(f"{self.GREEN}[1]{self.END} MBPP (Code Generation)")
|
| 126 |
+
print(f"{self.GREEN}[2]{self.END} HumanEval (Python)")
|
| 127 |
+
print(f"{self.GREEN}[3]{self.END} GSM8K (Math)")
|
| 128 |
+
print(f"{self.GREEN}[4]{self.END} Run All")
|
| 129 |
+
print(f"{self.GREEN}[0]{self.END} Back")
|
| 130 |
+
|
| 131 |
+
choice = input("\nSelect: ").strip()
|
| 132 |
+
|
| 133 |
+
if choice == '0':
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
benchmarks = {
|
| 137 |
+
'1': ('mbpp', 'MBPP'),
|
| 138 |
+
'2': ('human_eval', 'HumanEval'),
|
| 139 |
+
'3': ('gsm8k', 'GSM8K'),
|
| 140 |
+
'4': ('all', 'All')
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
if choice not in benchmarks:
|
| 144 |
+
print(f"{self.RED}Invalid choice{self.END}")
|
| 145 |
+
return
|
| 146 |
+
|
| 147 |
+
bench_name, bench_label = benchmarks[choice]
|
| 148 |
+
|
| 149 |
+
print(f"\n{self.YELLOW}Running {bench_label} benchmark...{self.END}")
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
if bench_name == 'all':
|
| 153 |
+
from benchmarks.mbpp import MBPP
|
| 154 |
+
from benchmarks.human_eval import HumanEval
|
| 155 |
+
from benchmarks.gsm8k import GSM8K
|
| 156 |
+
|
| 157 |
+
for name, Benchmark in [('MBPP', MBPP), ('HumanEval', HumanEval), ('GSM8K', GSM8K)]:
|
| 158 |
+
print(f"\n--- {name} ---")
|
| 159 |
+
b = Benchmark(model_provider=self.provider, model_name=self.model)
|
| 160 |
+
results = b.evaluate()
|
| 161 |
+
print(f" Accuracy: {results['accuracy']*100:.1f}%")
|
| 162 |
+
else:
|
| 163 |
+
module = __import__(f'benchmarks.{bench_name}', fromlist=['MBPP', 'HumanEval', 'GSM8K'])
|
| 164 |
+
Benchmark = getattr(module, bench_name.upper() if bench_name != 'mbpp' else 'MBPP')
|
| 165 |
+
b = Benchmark(model_provider=self.provider, model_name=self.model)
|
| 166 |
+
results = b.evaluate()
|
| 167 |
+
print(f"\n{self.GREEN}Results:{self.END}")
|
| 168 |
+
print(f" Accuracy: {results['accuracy']*100:.1f}%")
|
| 169 |
+
print(f" Passed: {results['pass_at_1']}/{results['total_cases']}")
|
| 170 |
+
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"{self.RED}Error: {e}{self.END}")
|
| 173 |
+
|
| 174 |
+
input("\nPress Enter to continue...")
|
| 175 |
+
|
| 176 |
+
def pattern_mode(self):
|
| 177 |
+
"""Manage patterns for self-evolution"""
|
| 178 |
+
print(f"\n{self.BLUE}=== Pattern Manager ==={self.END}")
|
| 179 |
+
print(f"{self.GREEN}[1]{self.END} View Patterns")
|
| 180 |
+
print(f"{self.GREEN}[2]{self.END} View Statistics")
|
| 181 |
+
print(f"{self.GREEN}[3]{self.END} Generate Synthetic Data")
|
| 182 |
+
print(f"{self.GREEN}[4]{self.END} Clear Patterns")
|
| 183 |
+
print(f"{self.GREEN}[0]{self.END} Back")
|
| 184 |
+
|
| 185 |
+
choice = input("\nSelect: ").strip()
|
| 186 |
+
|
| 187 |
+
if choice == '0':
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
if choice == '1':
|
| 191 |
+
patterns = self.miner.get_relevant_patterns(limit=20)
|
| 192 |
+
print(f"\n{self.YELLOW}Stored Patterns ({len(patterns)}):{self.END}")
|
| 193 |
+
for p in patterns:
|
| 194 |
+
print(f" [{p.pattern_type}] {p.code_snippet[:50]}... (rate: {p.success_rate:.0%})")
|
| 195 |
+
|
| 196 |
+
elif choice == '2':
|
| 197 |
+
stats = self.miner.get_statistics()
|
| 198 |
+
print(f"\n{self.YELLOW}Statistics:{self.END}")
|
| 199 |
+
print(f" Total Feedback: {stats['total_feedback']}")
|
| 200 |
+
print(f" Success Rate: {stats['success_rate']:.1%}")
|
| 201 |
+
print(f" Total Patterns: {stats['total_patterns']}")
|
| 202 |
+
print(f" By Type: {stats['patterns_by_type']}")
|
| 203 |
+
|
| 204 |
+
elif choice == '3':
|
| 205 |
+
try:
|
| 206 |
+
count = int(input("Number of examples: ").strip())
|
| 207 |
+
self.miner.store_feedback(
|
| 208 |
+
problem_type="synthetic",
|
| 209 |
+
solution="# Synthetic pattern",
|
| 210 |
+
success=True
|
| 211 |
+
)
|
| 212 |
+
print(f"{self.GREEN}✓{self.END} Generated {count} synthetic patterns")
|
| 213 |
+
except ValueError:
|
| 214 |
+
print(f"{self.RED}Invalid number{self.END}")
|
| 215 |
+
|
| 216 |
+
elif choice == '4':
|
| 217 |
+
confirm = input("Clear all patterns? (y/n): ").strip().lower()
|
| 218 |
+
if confirm == 'y':
|
| 219 |
+
# Note: This would need a clear method in PatternMiner
|
| 220 |
+
print(f"{self.YELLOW}Feature not implemented{self.END}")
|
| 221 |
+
|
| 222 |
+
input("\nPress Enter to continue...")
|
| 223 |
+
|
| 224 |
+
def train_mode(self):
|
| 225 |
+
"""Train model with LoRA"""
|
| 226 |
+
print(f"\n{self.BLUE}=== Training ==={self.END}")
|
| 227 |
+
print(f"{self.YELLOW}Note: Requires GPU and training data{self.END}")
|
| 228 |
+
print(f"\n{self.GREEN}[1]{self.END} Prepare Data")
|
| 229 |
+
print(f"{self.GREEN}[2]{self.END} Train LoRA")
|
| 230 |
+
print(f"{self.GREEN}[3]{self.END} Merge Adapter")
|
| 231 |
+
print(f"{self.GREEN}[0]{self.END} Back")
|
| 232 |
+
|
| 233 |
+
choice = input("\nSelect: ").strip()
|
| 234 |
+
|
| 235 |
+
if choice == '0':
|
| 236 |
+
return
|
| 237 |
+
|
| 238 |
+
if choice == '1':
|
| 239 |
+
print(f"\n{self.YELLOW}Preparing training data...{self.END}")
|
| 240 |
+
try:
|
| 241 |
+
from prepare_data import prepare_data
|
| 242 |
+
result = prepare_data()
|
| 243 |
+
print(f"{self.GREEN}✓{self.END} Prepared {result['train_samples']} training samples")
|
| 244 |
+
except Exception as e:
|
| 245 |
+
print(f"{self.RED}Error: {e}{self.END}")
|
| 246 |
+
|
| 247 |
+
elif choice == '2':
|
| 248 |
+
print(f"\n{YELLOW}Training LoRA...{self.END}")
|
| 249 |
+
print(f"{self.YELLOW}Note: This requires significant GPU resources{self.END}")
|
| 250 |
+
confirm = input("Continue? (y/n): ").strip().lower()
|
| 251 |
+
if confirm == 'y':
|
| 252 |
+
try:
|
| 253 |
+
from train_lora import train_lora
|
| 254 |
+
trainer = train_lora()
|
| 255 |
+
print(f"{self.GREEN}✓{self.END} Training complete")
|
| 256 |
+
except Exception as e:
|
| 257 |
+
print(f"{self.RED}Error: {e}{self.END}")
|
| 258 |
+
|
| 259 |
+
input("\nPress Enter to continue...")
|
| 260 |
+
|
| 261 |
+
def settings_mode(self):
|
| 262 |
+
"""Configure settings"""
|
| 263 |
+
print(f"\n{self.BLUE}=== Settings ==={self.END}")
|
| 264 |
+
print(f"Provider: {self.provider}")
|
| 265 |
+
print(f"Model: {self.model}")
|
| 266 |
+
print(f"\n{self.GREEN}[1]{self.END} Change Provider")
|
| 267 |
+
print(f"{self.GREEN}[2]{self.END} Change Model")
|
| 268 |
+
print(f"{self.GREEN}[0]{self.END} Back")
|
| 269 |
+
|
| 270 |
+
choice = input("\nSelect: ").strip()
|
| 271 |
+
|
| 272 |
+
if choice == '1':
|
| 273 |
+
print("Providers: ollama, openai, anthropic")
|
| 274 |
+
new_provider = input("Provider: ").strip()
|
| 275 |
+
if new_provider in ['ollama', 'openai', 'anthropic']:
|
| 276 |
+
self.provider = new_provider
|
| 277 |
+
self.init_client()
|
| 278 |
+
|
| 279 |
+
elif choice == '2':
|
| 280 |
+
new_model = input("Model name: ").strip()
|
| 281 |
+
if new_model:
|
| 282 |
+
self.model = new_model
|
| 283 |
+
self.init_client()
|
| 284 |
+
|
| 285 |
+
def run(self):
|
| 286 |
+
"""Run the CLI"""
|
| 287 |
+
self.print_header()
|
| 288 |
+
self.init_client()
|
| 289 |
+
|
| 290 |
+
while True:
|
| 291 |
+
self.print_menu()
|
| 292 |
+
choice = input(f"{self.GREEN}Select>{self.END} ").strip()
|
| 293 |
+
|
| 294 |
+
if choice == '0':
|
| 295 |
+
print(f"\n{self.BLUE}Thanks for using Stack 2.9!{self.END}\n")
|
| 296 |
+
break
|
| 297 |
+
|
| 298 |
+
if choice == '1':
|
| 299 |
+
self.chat_mode()
|
| 300 |
+
elif choice == '2':
|
| 301 |
+
self.eval_mode()
|
| 302 |
+
elif choice == '3':
|
| 303 |
+
self.pattern_mode()
|
| 304 |
+
elif choice == '4':
|
| 305 |
+
self.train_mode()
|
| 306 |
+
elif choice == '5':
|
| 307 |
+
self.settings_mode()
|
| 308 |
+
else:
|
| 309 |
+
print(f"{self.RED}Invalid option{self.END}")
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def main():
|
| 313 |
+
parser = argparse.ArgumentParser(description="Stack 2.9 CLI")
|
| 314 |
+
parser.add_argument("--provider", "-p", choices=["ollama", "openai", "anthropic"],
|
| 315 |
+
help="Model provider")
|
| 316 |
+
parser.add_argument("--model", "-m", type=str, help="Model name")
|
| 317 |
+
parser.add_argument("--chat", "-c", action="store_true", help="Start in chat mode")
|
| 318 |
+
parser.add_argument("--eval", "-e", choices=["mbpp", "human_eval", "gsm8k", "all"],
|
| 319 |
+
help="Run evaluation")
|
| 320 |
+
|
| 321 |
+
args = parser.parse_args()
|
| 322 |
+
|
| 323 |
+
cli = Stack29CLI(provider=args.provider, model=args.model)
|
| 324 |
+
|
| 325 |
+
if args.chat:
|
| 326 |
+
cli.init_client()
|
| 327 |
+
cli.chat_mode()
|
| 328 |
+
elif args.eval:
|
| 329 |
+
cli.init_client()
|
| 330 |
+
cli.eval_mode()
|
| 331 |
+
else:
|
| 332 |
+
cli.run()
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
if __name__ == "__main__":
|
| 336 |
+
main()
|
archived-selective/cli-backup/pyproject.toml
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "stack-cli"
|
| 7 |
+
version = "2.9.0"
|
| 8 |
+
description = "Stack 2.9 CLI and Agent Interface"
|
| 9 |
+
readme = "../STACK_CLI_README.md"
|
| 10 |
+
license = {text = "MIT"}
|
| 11 |
+
authors = [
|
| 12 |
+
{name = "Walid Sobhi", email = "walid@example.com"}
|
| 13 |
+
]
|
| 14 |
+
keywords = ["cli", "agent", "ai", "automation", "development"]
|
| 15 |
+
classifiers = [
|
| 16 |
+
"Development Status :: 3 - Alpha",
|
| 17 |
+
"Intended Audience :: Developers",
|
| 18 |
+
"License :: OSI Approved :: MIT License",
|
| 19 |
+
"Programming Language :: Python :: 3",
|
| 20 |
+
"Programming Language :: Python :: 3.8",
|
| 21 |
+
"Programming Language :: Python :: 3.9",
|
| 22 |
+
"Programming Language :: Python :: 3.10",
|
| 23 |
+
"Programming Language :: Python :: 3.11",
|
| 24 |
+
"Topic :: Software Development :: Tools",
|
| 25 |
+
]
|
| 26 |
+
requires-python = ">=3.8"
|
| 27 |
+
dependencies = [
|
| 28 |
+
"openai>=1.0.0",
|
| 29 |
+
"openrouter>=1.0.0",
|
| 30 |
+
"anthropic>=0.8.0",
|
| 31 |
+
"aiohttp>=3.9.0",
|
| 32 |
+
"python-dotenv>=1.0.0",
|
| 33 |
+
"prompt-toolkit>=3.0.0",
|
| 34 |
+
"rich>=13.0.0",
|
| 35 |
+
"click>=8.0.0",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
[project.optional-dependencies]
|
| 39 |
+
dev = [
|
| 40 |
+
"pytest>=7.0.0",
|
| 41 |
+
"pytest-cov>=4.0.0",
|
| 42 |
+
"ruff>=0.1.0",
|
| 43 |
+
"mypy>=1.0.0",
|
| 44 |
+
]
|
| 45 |
+
voice = [
|
| 46 |
+
"SpeechRecognition>=3.10.0",
|
| 47 |
+
"pyttsx3>=2.90",
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
[project.scripts]
|
| 51 |
+
stack = "stack_cli.cli:main"
|
| 52 |
+
|
| 53 |
+
[tool.setuptools.packages.find]
|
| 54 |
+
where = ["."]
|
| 55 |
+
include = ["stack_cli*"]
|
| 56 |
+
|
| 57 |
+
[tool.black]
|
| 58 |
+
line-length = 100
|
| 59 |
+
target-version = ['py38', 'py39', 'py310', 'py311']
|
| 60 |
+
include = '\.pyi?$'
|
| 61 |
+
|
| 62 |
+
[tool.ruff]
|
| 63 |
+
line-length = 100
|
| 64 |
+
target-version = "py38"
|
| 65 |
+
|
| 66 |
+
[tool.mypy]
|
| 67 |
+
python_version = "3.8"
|
| 68 |
+
warn_return_any = true
|
| 69 |
+
warn_unused_configs = true
|
| 70 |
+
disallow_untyped_defs = false
|
archived-selective/cli-backup/tools.py
ADDED
|
@@ -0,0 +1,1308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Stack 2.9 - Built-in Tools Module
|
| 4 |
+
38 powerful tools for file operations, git, code execution, web, memory, and planning.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import json
|
| 10 |
+
import subprocess
|
| 11 |
+
import shutil
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any, Callable, Dict, List, Optional, Union
|
| 14 |
+
from datetime import datetime, timedelta
|
| 15 |
+
import hashlib
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# FILE OPERATIONS TOOLS (Tools 1-8)
|
| 20 |
+
# ============================================================================
|
| 21 |
+
|
| 22 |
+
def tool_read_file(path: str, offset: int = 0, limit: int = -1) -> Dict[str, Any]:
|
| 23 |
+
"""Read file contents with optional offset and limit."""
|
| 24 |
+
try:
|
| 25 |
+
p = Path(path)
|
| 26 |
+
if not p.exists():
|
| 27 |
+
return {"success": False, "error": f"File not found: {path}"}
|
| 28 |
+
|
| 29 |
+
content = p.read_text(encoding='utf-8')
|
| 30 |
+
lines = content.split('\n')
|
| 31 |
+
|
| 32 |
+
if limit > 0:
|
| 33 |
+
lines = lines[offset:offset + limit]
|
| 34 |
+
|
| 35 |
+
return {
|
| 36 |
+
"success": True,
|
| 37 |
+
"content": '\n'.join(lines),
|
| 38 |
+
"total_lines": len(content.split('\n')),
|
| 39 |
+
"path": path
|
| 40 |
+
}
|
| 41 |
+
except Exception as e:
|
| 42 |
+
return {"success": False, "error": str(e)}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def tool_write_file(path: str, content: str, append: bool = False) -> Dict[str, Any]:
|
| 46 |
+
"""Write content to file (create or overwrite)."""
|
| 47 |
+
try:
|
| 48 |
+
p = Path(path)
|
| 49 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
|
| 51 |
+
if append:
|
| 52 |
+
p.write_text(content, encoding='utf-8')
|
| 53 |
+
else:
|
| 54 |
+
p.write_text(content, encoding='utf-8')
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
"success": True,
|
| 58 |
+
"path": path,
|
| 59 |
+
"lines_written": len(content.split('\n'))
|
| 60 |
+
}
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return {"success": False, "error": str(e)}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def tool_edit_file(path: str, old_text: str, new_text: str) -> Dict[str, Any]:
|
| 66 |
+
"""Edit file using exact text replacement."""
|
| 67 |
+
try:
|
| 68 |
+
p = Path(path)
|
| 69 |
+
if not p.exists():
|
| 70 |
+
return {"success": False, "error": f"File not found: {path}"}
|
| 71 |
+
|
| 72 |
+
content = p.read_text(encoding='utf-8')
|
| 73 |
+
if old_text not in content:
|
| 74 |
+
return {"success": False, "error": "Text to replace not found"}
|
| 75 |
+
|
| 76 |
+
new_content = content.replace(old_text, new_text, 1)
|
| 77 |
+
p.write_text(new_content, encoding='utf-8')
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
"success": True,
|
| 81 |
+
"path": path,
|
| 82 |
+
"edits_made": 1
|
| 83 |
+
}
|
| 84 |
+
except Exception as e:
|
| 85 |
+
return {"success": False, "error": str(e)}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def tool_search_files(
|
| 89 |
+
path: str,
|
| 90 |
+
pattern: str,
|
| 91 |
+
exclude: Optional[List[str]] = None
|
| 92 |
+
) -> Dict[str, Any]:
|
| 93 |
+
"""Recursively search for files matching a pattern."""
|
| 94 |
+
try:
|
| 95 |
+
# Expand ~ to home directory
|
| 96 |
+
base_path = Path(os.path.expanduser(path))
|
| 97 |
+
if not base_path.exists():
|
| 98 |
+
return {"success": False, "error": f"Path not found: {path}"}
|
| 99 |
+
|
| 100 |
+
results = []
|
| 101 |
+
exclude = exclude or []
|
| 102 |
+
|
| 103 |
+
for p in base_path.rglob(pattern):
|
| 104 |
+
# Check if any exclusion pattern matches
|
| 105 |
+
skip = False
|
| 106 |
+
for exc in exclude:
|
| 107 |
+
if exc in str(p):
|
| 108 |
+
skip = True
|
| 109 |
+
break
|
| 110 |
+
if not skip:
|
| 111 |
+
results.append(str(p))
|
| 112 |
+
|
| 113 |
+
return {
|
| 114 |
+
"success": True,
|
| 115 |
+
"matches": results,
|
| 116 |
+
"count": len(results)
|
| 117 |
+
}
|
| 118 |
+
except Exception as e:
|
| 119 |
+
return {"success": False, "error": str(e)}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def tool_grep(path: str, pattern: str, context: int = 0) -> Dict[str, Any]:
|
| 123 |
+
"""Search for pattern in file(s)."""
|
| 124 |
+
try:
|
| 125 |
+
# Expand ~ to home directory
|
| 126 |
+
base_path = Path(os.path.expanduser(path))
|
| 127 |
+
results = []
|
| 128 |
+
|
| 129 |
+
if base_path.is_file():
|
| 130 |
+
files = [base_path]
|
| 131 |
+
elif base_path.is_dir():
|
| 132 |
+
files = list(base_path.rglob('*'))
|
| 133 |
+
files = [f for f in files if f.is_file()]
|
| 134 |
+
else:
|
| 135 |
+
return {"success": False, "error": f"Invalid path: {path}"}
|
| 136 |
+
|
| 137 |
+
for f in files:
|
| 138 |
+
try:
|
| 139 |
+
content = f.read_text(encoding='utf-8', errors='ignore')
|
| 140 |
+
lines = content.split('\n')
|
| 141 |
+
|
| 142 |
+
for i, line in enumerate(lines):
|
| 143 |
+
if re.search(pattern, line):
|
| 144 |
+
result = {
|
| 145 |
+
"file": str(f),
|
| 146 |
+
"line": i + 1,
|
| 147 |
+
"content": line.strip()
|
| 148 |
+
}
|
| 149 |
+
if context > 0:
|
| 150 |
+
start = max(0, i - context)
|
| 151 |
+
end = min(len(lines), i + context + 1)
|
| 152 |
+
result["context"] = lines[start:end]
|
| 153 |
+
results.append(result)
|
| 154 |
+
except:
|
| 155 |
+
continue
|
| 156 |
+
|
| 157 |
+
return {
|
| 158 |
+
"success": True,
|
| 159 |
+
"matches": results,
|
| 160 |
+
"count": len(results)
|
| 161 |
+
}
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return {"success": False, "error": str(e)}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def tool_copy_file(source: str, destination: str) -> Dict[str, Any]:
|
| 167 |
+
"""Copy file or directory."""
|
| 168 |
+
try:
|
| 169 |
+
src = Path(source)
|
| 170 |
+
dst = Path(destination)
|
| 171 |
+
|
| 172 |
+
if not src.exists():
|
| 173 |
+
return {"success": False, "error": f"Source not found: {source}"}
|
| 174 |
+
|
| 175 |
+
if src.is_dir():
|
| 176 |
+
shutil.copytree(src, dst)
|
| 177 |
+
else:
|
| 178 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 179 |
+
shutil.copy2(src, dst)
|
| 180 |
+
|
| 181 |
+
return {
|
| 182 |
+
"success": True,
|
| 183 |
+
"source": source,
|
| 184 |
+
"destination": destination
|
| 185 |
+
}
|
| 186 |
+
except Exception as e:
|
| 187 |
+
return {"success": False, "error": str(e)}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def tool_move_file(source: str, destination: str) -> Dict[str, Any]:
|
| 191 |
+
"""Move or rename file or directory."""
|
| 192 |
+
try:
|
| 193 |
+
src = Path(source)
|
| 194 |
+
dst = Path(destination)
|
| 195 |
+
|
| 196 |
+
if not src.exists():
|
| 197 |
+
return {"success": False, "error": f"Source not found: {source}"}
|
| 198 |
+
|
| 199 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 200 |
+
shutil.move(str(src), str(dst))
|
| 201 |
+
|
| 202 |
+
return {
|
| 203 |
+
"success": True,
|
| 204 |
+
"source": source,
|
| 205 |
+
"destination": destination
|
| 206 |
+
}
|
| 207 |
+
except Exception as e:
|
| 208 |
+
return {"success": False, "error": str(e)}
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def tool_delete_file(path: str, force: bool = False) -> Dict[str, Any]:
|
| 212 |
+
"""Delete file or directory (use trash for safe delete)."""
|
| 213 |
+
try:
|
| 214 |
+
p = Path(path)
|
| 215 |
+
|
| 216 |
+
if not p.exists():
|
| 217 |
+
return {"success": False, "error": f"Path not found: {path}"}
|
| 218 |
+
|
| 219 |
+
# For safety, require force=True for destructive delete
|
| 220 |
+
if not force:
|
| 221 |
+
# Just report what would be deleted
|
| 222 |
+
return {
|
| 223 |
+
"success": True,
|
| 224 |
+
"would_delete": str(p),
|
| 225 |
+
"warning": "Set force=True to actually delete"
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
if p.is_dir():
|
| 229 |
+
shutil.rmtree(p)
|
| 230 |
+
else:
|
| 231 |
+
p.unlink()
|
| 232 |
+
|
| 233 |
+
return {
|
| 234 |
+
"success": True,
|
| 235 |
+
"deleted": str(p)
|
| 236 |
+
}
|
| 237 |
+
except Exception as e:
|
| 238 |
+
return {"success": False, "error": str(e)}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ============================================================================
|
| 242 |
+
# GIT OPERATIONS TOOLS (Tools 9-15)
|
| 243 |
+
# ============================================================================
|
| 244 |
+
|
| 245 |
+
def tool_git_status(repo_path: str = ".") -> Dict[str, Any]:
|
| 246 |
+
"""Get git status."""
|
| 247 |
+
try:
|
| 248 |
+
result = subprocess.run(
|
| 249 |
+
["git", "-C", repo_path, "status", "--porcelain"],
|
| 250 |
+
capture_output=True,
|
| 251 |
+
text=True,
|
| 252 |
+
timeout=30
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
files = [line[3:] for line in result.stdout.strip().split('\n') if line]
|
| 256 |
+
|
| 257 |
+
return {
|
| 258 |
+
"success": True,
|
| 259 |
+
"files": files,
|
| 260 |
+
"count": len(files),
|
| 261 |
+
"repo": repo_path
|
| 262 |
+
}
|
| 263 |
+
except Exception as e:
|
| 264 |
+
return {"success": False, "error": str(e)}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def tool_git_commit(repo_path: str, message: str, files: Optional[List[str]] = None) -> Dict[str, Any]:
|
| 268 |
+
"""Create a git commit."""
|
| 269 |
+
try:
|
| 270 |
+
# Stage files if provided
|
| 271 |
+
if files:
|
| 272 |
+
for f in files:
|
| 273 |
+
subprocess.run(
|
| 274 |
+
["git", "-C", repo_path, "add", f],
|
| 275 |
+
capture_output=True,
|
| 276 |
+
timeout=30
|
| 277 |
+
)
|
| 278 |
+
else:
|
| 279 |
+
subprocess.run(
|
| 280 |
+
["git", "-C", repo_path, "add", "-A"],
|
| 281 |
+
capture_output=True,
|
| 282 |
+
timeout=30
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# Check if there are changes to commit
|
| 286 |
+
result = subprocess.run(
|
| 287 |
+
["git", "-C", repo_path, "status", "--porcelain"],
|
| 288 |
+
capture_output=True,
|
| 289 |
+
text=True,
|
| 290 |
+
timeout=30
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
if not result.stdout.strip():
|
| 294 |
+
return {"success": True, "message": "No changes to commit"}
|
| 295 |
+
|
| 296 |
+
# Commit
|
| 297 |
+
result = subprocess.run(
|
| 298 |
+
["git", "-C", repo_path, "commit", "-m", message],
|
| 299 |
+
capture_output=True,
|
| 300 |
+
text=True,
|
| 301 |
+
timeout=30
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
return {
|
| 305 |
+
"success": True,
|
| 306 |
+
"message": message,
|
| 307 |
+
"output": result.stdout + result.stderr
|
| 308 |
+
}
|
| 309 |
+
except Exception as e:
|
| 310 |
+
return {"success": False, "error": str(e)}
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def tool_git_push(repo_path: str = ".", remote: str = "origin", branch: Optional[str] = None) -> Dict[str, Any]:
|
| 314 |
+
"""Push to remote."""
|
| 315 |
+
try:
|
| 316 |
+
cmd = ["git", "-C", repo_path, "push", remote]
|
| 317 |
+
if branch:
|
| 318 |
+
cmd.append(branch)
|
| 319 |
+
|
| 320 |
+
result = subprocess.run(
|
| 321 |
+
cmd,
|
| 322 |
+
capture_output=True,
|
| 323 |
+
text=True,
|
| 324 |
+
timeout=60
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
return {
|
| 328 |
+
"success": True,
|
| 329 |
+
"remote": remote,
|
| 330 |
+
"branch": branch,
|
| 331 |
+
"output": result.stdout + result.stderr
|
| 332 |
+
}
|
| 333 |
+
except Exception as e:
|
| 334 |
+
return {"success": False, "error": str(e)}
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def tool_git_pull(repo_path: str = ".", remote: str = "origin", branch: Optional[str] = None) -> Dict[str, Any]:
|
| 338 |
+
"""Pull from remote."""
|
| 339 |
+
try:
|
| 340 |
+
cmd = ["git", "-C", repo_path, "pull", remote]
|
| 341 |
+
if branch:
|
| 342 |
+
cmd.append(branch)
|
| 343 |
+
|
| 344 |
+
result = subprocess.run(
|
| 345 |
+
cmd,
|
| 346 |
+
capture_output=True,
|
| 347 |
+
text=True,
|
| 348 |
+
timeout=60
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
return {
|
| 352 |
+
"success": True,
|
| 353 |
+
"remote": remote,
|
| 354 |
+
"branch": branch,
|
| 355 |
+
"output": result.stdout + result.stderr
|
| 356 |
+
}
|
| 357 |
+
except Exception as e:
|
| 358 |
+
return {"success": False, "error": str(e)}
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def tool_git_branch(repo_path: str = ".", create: Optional[str] = None, delete: Optional[str] = None) -> Dict[str, Any]:
|
| 362 |
+
"""List, create, or delete branches."""
|
| 363 |
+
try:
|
| 364 |
+
if create:
|
| 365 |
+
result = subprocess.run(
|
| 366 |
+
["git", "-C", repo_path, "checkout", "-b", create],
|
| 367 |
+
capture_output=True,
|
| 368 |
+
text=True,
|
| 369 |
+
timeout=30
|
| 370 |
+
)
|
| 371 |
+
return {"success": True, "created": create}
|
| 372 |
+
|
| 373 |
+
if delete:
|
| 374 |
+
result = subprocess.run(
|
| 375 |
+
["git", "-C", repo_path, "branch", "-D", delete],
|
| 376 |
+
capture_output=True,
|
| 377 |
+
text=True,
|
| 378 |
+
timeout=30
|
| 379 |
+
)
|
| 380 |
+
return {"success": True, "deleted": delete}
|
| 381 |
+
|
| 382 |
+
# List branches
|
| 383 |
+
result = subprocess.run(
|
| 384 |
+
["git", "-C", repo_path, "branch", "-a"],
|
| 385 |
+
capture_output=True,
|
| 386 |
+
text=True,
|
| 387 |
+
timeout=30
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
branches = [b.strip().replace('* ', '') for b in result.stdout.strip().split('\n') if b]
|
| 391 |
+
|
| 392 |
+
return {
|
| 393 |
+
"success": True,
|
| 394 |
+
"branches": branches,
|
| 395 |
+
"count": len(branches)
|
| 396 |
+
}
|
| 397 |
+
except Exception as e:
|
| 398 |
+
return {"success": False, "error": str(e)}
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def tool_git_log(repo_path: str = ".", limit: int = 10) -> Dict[str, Any]:
|
| 402 |
+
"""Get git log."""
|
| 403 |
+
try:
|
| 404 |
+
result = subprocess.run(
|
| 405 |
+
["git", "-C", repo_path, "log", f"--max-count={limit}", "--oneline"],
|
| 406 |
+
capture_output=True,
|
| 407 |
+
text=True,
|
| 408 |
+
timeout=30
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
commits = result.stdout.strip().split('\n')
|
| 412 |
+
|
| 413 |
+
return {
|
| 414 |
+
"success": True,
|
| 415 |
+
"commits": commits,
|
| 416 |
+
"count": len([c for c in commits if c])
|
| 417 |
+
}
|
| 418 |
+
except Exception as e:
|
| 419 |
+
return {"success": False, "error": str(e)}
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def tool_git_diff(repo_path: str = ".", file: Optional[str] = None, staged: bool = False) -> Dict[str, Any]:
|
| 423 |
+
"""Get git diff."""
|
| 424 |
+
try:
|
| 425 |
+
cmd = ["git", "-C", repo_path, "diff"]
|
| 426 |
+
if staged:
|
| 427 |
+
cmd.append("--staged")
|
| 428 |
+
if file:
|
| 429 |
+
cmd.append(file)
|
| 430 |
+
|
| 431 |
+
result = subprocess.run(
|
| 432 |
+
cmd,
|
| 433 |
+
capture_output=True,
|
| 434 |
+
text=True,
|
| 435 |
+
timeout=30
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
return {
|
| 439 |
+
"success": True,
|
| 440 |
+
"diff": result.stdout,
|
| 441 |
+
"has_changes": bool(result.stdout.strip())
|
| 442 |
+
}
|
| 443 |
+
except Exception as e:
|
| 444 |
+
return {"success": False, "error": str(e)}
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
# ============================================================================
|
| 448 |
+
# CODE EXECUTION TOOLS (Tools 16-22)
|
| 449 |
+
# ============================================================================
|
| 450 |
+
|
| 451 |
+
def tool_run_command(
|
| 452 |
+
command: str,
|
| 453 |
+
timeout: int = 60,
|
| 454 |
+
cwd: Optional[str] = None,
|
| 455 |
+
env: Optional[Dict[str, str]] = None
|
| 456 |
+
) -> Dict[str, Any]:
|
| 457 |
+
"""Run shell command."""
|
| 458 |
+
try:
|
| 459 |
+
result = subprocess.run(
|
| 460 |
+
command,
|
| 461 |
+
shell=True,
|
| 462 |
+
capture_output=True,
|
| 463 |
+
text=True,
|
| 464 |
+
timeout=timeout,
|
| 465 |
+
cwd=cwd,
|
| 466 |
+
env={**os.environ, **(env or {})}
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
return {
|
| 470 |
+
"success": result.returncode == 0,
|
| 471 |
+
"returncode": result.returncode,
|
| 472 |
+
"stdout": result.stdout,
|
| 473 |
+
"stderr": result.stderr,
|
| 474 |
+
"command": command
|
| 475 |
+
}
|
| 476 |
+
except subprocess.TimeoutExpired:
|
| 477 |
+
return {"success": False, "error": "Command timed out"}
|
| 478 |
+
except Exception as e:
|
| 479 |
+
return {"success": False, "error": str(e)}
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def tool_run_tests(path: str = ".", pattern: str = "test*.py", verbose: bool = True) -> Dict[str, Any]:
|
| 483 |
+
"""Run tests using pytest."""
|
| 484 |
+
try:
|
| 485 |
+
cmd = ["pytest", path, "-k", pattern]
|
| 486 |
+
if verbose:
|
| 487 |
+
cmd.append("-v")
|
| 488 |
+
|
| 489 |
+
result = subprocess.run(
|
| 490 |
+
cmd,
|
| 491 |
+
capture_output=True,
|
| 492 |
+
text=True,
|
| 493 |
+
timeout=300,
|
| 494 |
+
cwd=path
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
return {
|
| 498 |
+
"success": result.returncode == 0,
|
| 499 |
+
"output": result.stdout,
|
| 500 |
+
"errors": result.stderr,
|
| 501 |
+
"returncode": result.returncode
|
| 502 |
+
}
|
| 503 |
+
except FileNotFoundError:
|
| 504 |
+
return {"success": False, "error": "pytest not found"}
|
| 505 |
+
except Exception as e:
|
| 506 |
+
return {"success": False, "error": str(e)}
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def tool_lint_code(path: str = ".", linter: str = "ruff") -> Dict[str, Any]:
|
| 510 |
+
"""Lint code."""
|
| 511 |
+
try:
|
| 512 |
+
if linter == "ruff":
|
| 513 |
+
result = subprocess.run(
|
| 514 |
+
["ruff", "check", path],
|
| 515 |
+
capture_output=True,
|
| 516 |
+
text=True,
|
| 517 |
+
timeout=120
|
| 518 |
+
)
|
| 519 |
+
elif linter == "pylint":
|
| 520 |
+
result = subprocess.run(
|
| 521 |
+
["pylint", path],
|
| 522 |
+
capture_output=True,
|
| 523 |
+
text=True,
|
| 524 |
+
timeout=120
|
| 525 |
+
)
|
| 526 |
+
elif linter == "mypy":
|
| 527 |
+
result = subprocess.run(
|
| 528 |
+
["mypy", path],
|
| 529 |
+
capture_output=True,
|
| 530 |
+
text=True,
|
| 531 |
+
timeout=120
|
| 532 |
+
)
|
| 533 |
+
else:
|
| 534 |
+
return {"success": False, "error": f"Unknown linter: {linter}"}
|
| 535 |
+
|
| 536 |
+
return {
|
| 537 |
+
"success": result.returncode == 0,
|
| 538 |
+
"output": result.stdout,
|
| 539 |
+
"errors": result.stderr
|
| 540 |
+
}
|
| 541 |
+
except FileNotFoundError:
|
| 542 |
+
return {"success": False, "error": f"{linter} not found"}
|
| 543 |
+
except Exception as e:
|
| 544 |
+
return {"success": False, "error": str(e)}
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def tool_format_code(path: str = ".", formatter: str = "ruff") -> Dict[str, Any]:
|
| 548 |
+
"""Format code."""
|
| 549 |
+
try:
|
| 550 |
+
if formatter == "ruff":
|
| 551 |
+
result = subprocess.run(
|
| 552 |
+
["ruff", "format", path],
|
| 553 |
+
capture_output=True,
|
| 554 |
+
text=True,
|
| 555 |
+
timeout=120
|
| 556 |
+
)
|
| 557 |
+
elif formatter == "black":
|
| 558 |
+
result = subprocess.run(
|
| 559 |
+
["black", path],
|
| 560 |
+
capture_output=True,
|
| 561 |
+
text=True,
|
| 562 |
+
timeout=120
|
| 563 |
+
)
|
| 564 |
+
else:
|
| 565 |
+
return {"success": False, "error": f"Unknown formatter: {formatter}"}
|
| 566 |
+
|
| 567 |
+
return {
|
| 568 |
+
"success": result.returncode == 0,
|
| 569 |
+
"output": result.stdout,
|
| 570 |
+
"errors": result.stderr
|
| 571 |
+
}
|
| 572 |
+
except FileNotFoundError:
|
| 573 |
+
return {"success": False, "error": f"{formatter} not found"}
|
| 574 |
+
except Exception as e:
|
| 575 |
+
return {"success": False, "error": str(e)}
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
def tool_check_type(path: str = ".") -> Dict[str, Any]:
|
| 579 |
+
"""Type check with mypy."""
|
| 580 |
+
try:
|
| 581 |
+
result = subprocess.run(
|
| 582 |
+
["mypy", path, "--ignore-missing-imports"],
|
| 583 |
+
capture_output=True,
|
| 584 |
+
text=True,
|
| 585 |
+
timeout=180
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
return {
|
| 589 |
+
"success": result.returncode == 0,
|
| 590 |
+
"output": result.stdout,
|
| 591 |
+
"errors": result.stderr
|
| 592 |
+
}
|
| 593 |
+
except FileNotFoundError:
|
| 594 |
+
return {"success": False, "error": "mypy not found"}
|
| 595 |
+
except Exception as e:
|
| 596 |
+
return {"success": False, "error": str(e)}
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def tool_start_server(
|
| 600 |
+
command: str,
|
| 601 |
+
port: int,
|
| 602 |
+
cwd: Optional[str] = None,
|
| 603 |
+
background: bool = False
|
| 604 |
+
) -> Dict[str, Any]:
|
| 605 |
+
"""Start a development server."""
|
| 606 |
+
try:
|
| 607 |
+
if background:
|
| 608 |
+
proc = subprocess.Popen(
|
| 609 |
+
command,
|
| 610 |
+
shell=True,
|
| 611 |
+
cwd=cwd,
|
| 612 |
+
stdout=subprocess.PIPE,
|
| 613 |
+
stderr=subprocess.PIPE
|
| 614 |
+
)
|
| 615 |
+
return {
|
| 616 |
+
"success": True,
|
| 617 |
+
"pid": proc.pid,
|
| 618 |
+
"port": port,
|
| 619 |
+
"message": f"Server started on port {port}"
|
| 620 |
+
}
|
| 621 |
+
else:
|
| 622 |
+
result = subprocess.run(
|
| 623 |
+
command,
|
| 624 |
+
shell=True,
|
| 625 |
+
capture_output=True,
|
| 626 |
+
text=True,
|
| 627 |
+
cwd=cwd
|
| 628 |
+
)
|
| 629 |
+
return {
|
| 630 |
+
"success": result.returncode == 0,
|
| 631 |
+
"output": result.stdout
|
| 632 |
+
}
|
| 633 |
+
except Exception as e:
|
| 634 |
+
return {"success": False, "error": str(e)}
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def tool_install_dependencies(path: str = ".", package_manager: str = "pip") -> Dict[str, Any]:
|
| 638 |
+
"""Install dependencies."""
|
| 639 |
+
try:
|
| 640 |
+
if package_manager == "pip":
|
| 641 |
+
result = subprocess.run(
|
| 642 |
+
["pip", "install", "-r", "requirements.txt"],
|
| 643 |
+
capture_output=True,
|
| 644 |
+
text=True,
|
| 645 |
+
timeout=300,
|
| 646 |
+
cwd=path
|
| 647 |
+
)
|
| 648 |
+
elif package_manager == "poetry":
|
| 649 |
+
result = subprocess.run(
|
| 650 |
+
["poetry", "install"],
|
| 651 |
+
capture_output=True,
|
| 652 |
+
text=True,
|
| 653 |
+
timeout=300,
|
| 654 |
+
cwd=path
|
| 655 |
+
)
|
| 656 |
+
elif package_manager == "npm":
|
| 657 |
+
result = subprocess.run(
|
| 658 |
+
["npm", "install"],
|
| 659 |
+
capture_output=True,
|
| 660 |
+
text=True,
|
| 661 |
+
timeout=300,
|
| 662 |
+
cwd=path
|
| 663 |
+
)
|
| 664 |
+
else:
|
| 665 |
+
return {"success": False, "error": f"Unknown package manager: {package_manager}"}
|
| 666 |
+
|
| 667 |
+
return {
|
| 668 |
+
"success": result.returncode == 0,
|
| 669 |
+
"output": result.stdout,
|
| 670 |
+
"errors": result.stderr
|
| 671 |
+
}
|
| 672 |
+
except Exception as e:
|
| 673 |
+
return {"success": False, "error": str(e)}
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
# ============================================================================
|
| 677 |
+
# WEB TOOLS (Tools 23-27)
|
| 678 |
+
# ============================================================================
|
| 679 |
+
|
| 680 |
+
def tool_web_search(
|
| 681 |
+
query: str,
|
| 682 |
+
count: int = 5,
|
| 683 |
+
freshness: Optional[str] = None,
|
| 684 |
+
language: Optional[str] = None
|
| 685 |
+
) -> Dict[str, Any]:
|
| 686 |
+
"""Search the web using DuckDuckGo."""
|
| 687 |
+
try:
|
| 688 |
+
import urllib.request
|
| 689 |
+
import urllib.parse
|
| 690 |
+
import re
|
| 691 |
+
from html import unescape
|
| 692 |
+
|
| 693 |
+
# DuckDuckGo Lite
|
| 694 |
+
encoded_query = urllib.parse.quote(query)
|
| 695 |
+
url = f"https://lite.duckduckgo.com/lite/?q={encoded_query}"
|
| 696 |
+
|
| 697 |
+
req = urllib.request.Request(url, headers={
|
| 698 |
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
| 699 |
+
})
|
| 700 |
+
with urllib.request.urlopen(req, timeout=30) as response:
|
| 701 |
+
html = response.read().decode('utf-8')
|
| 702 |
+
|
| 703 |
+
results = []
|
| 704 |
+
# Find links - look for anchor tags with titles
|
| 705 |
+
all_links = re.findall(r'<a[^>]*href="(https?://[^"]+)"[^>]*>([^<]+)</a>', html)
|
| 706 |
+
|
| 707 |
+
for url, title in all_links[:count]:
|
| 708 |
+
title = unescape(title).strip()
|
| 709 |
+
if title and len(title) > 3:
|
| 710 |
+
results.append({"title": title, "url": url})
|
| 711 |
+
|
| 712 |
+
return {
|
| 713 |
+
"success": True,
|
| 714 |
+
"query": query,
|
| 715 |
+
"results": results[:count],
|
| 716 |
+
"count": len(results)
|
| 717 |
+
}
|
| 718 |
+
except Exception as e:
|
| 719 |
+
return {"success": False, "error": str(e)}
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
def tool_web_fetch(url: str, max_chars: int = 10000) -> Dict[str, Any]:
|
| 723 |
+
"""Fetch and extract content from URL."""
|
| 724 |
+
try:
|
| 725 |
+
result = subprocess.run(
|
| 726 |
+
["curl", "-s", url],
|
| 727 |
+
capture_output=True,
|
| 728 |
+
text=True,
|
| 729 |
+
timeout=30
|
| 730 |
+
)
|
| 731 |
+
|
| 732 |
+
content = result.stdout[:max_chars]
|
| 733 |
+
|
| 734 |
+
return {
|
| 735 |
+
"success": True,
|
| 736 |
+
"url": url,
|
| 737 |
+
"content": content,
|
| 738 |
+
"length": len(content)
|
| 739 |
+
}
|
| 740 |
+
except Exception as e:
|
| 741 |
+
return {"success": False, "error": str(e)}
|
| 742 |
+
|
| 743 |
+
|
| 744 |
+
def tool_download_file(url: str, destination: str) -> Dict[str, Any]:
|
| 745 |
+
"""Download file from URL."""
|
| 746 |
+
try:
|
| 747 |
+
result = subprocess.run(
|
| 748 |
+
["curl", "-L", "-o", destination, url],
|
| 749 |
+
capture_output=True,
|
| 750 |
+
text=True,
|
| 751 |
+
timeout=120
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
size = Path(destination).stat().st_size if Path(destination).exists() else 0
|
| 755 |
+
|
| 756 |
+
return {
|
| 757 |
+
"success": result.returncode == 0,
|
| 758 |
+
"url": url,
|
| 759 |
+
"destination": destination,
|
| 760 |
+
"size": size
|
| 761 |
+
}
|
| 762 |
+
except Exception as e:
|
| 763 |
+
return {"success": False, "error": str(e)}
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def tool_check_url(url: str) -> Dict[str, Any]:
|
| 767 |
+
"""Check if URL is accessible."""
|
| 768 |
+
try:
|
| 769 |
+
result = subprocess.run(
|
| 770 |
+
["curl", "-I", "-s", "-o", "/dev/null", "-w", "%{http_code}", url],
|
| 771 |
+
capture_output=True,
|
| 772 |
+
text=True,
|
| 773 |
+
timeout=15
|
| 774 |
+
)
|
| 775 |
+
|
| 776 |
+
code = result.stdout.strip()
|
| 777 |
+
|
| 778 |
+
return {
|
| 779 |
+
"success": code in ["200", "301", "302"],
|
| 780 |
+
"url": url,
|
| 781 |
+
"status_code": code
|
| 782 |
+
}
|
| 783 |
+
except Exception as e:
|
| 784 |
+
return {"success": False, "error": str(e)}
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def tool_screenshot(url: str, destination: str = "screenshot.png") -> Dict[str, Any]:
|
| 788 |
+
"""Take screenshot of webpage."""
|
| 789 |
+
try:
|
| 790 |
+
# Try playwright or puppeteer
|
| 791 |
+
result = subprocess.run(
|
| 792 |
+
["npx", "puppeteer", url, "--output", destination],
|
| 793 |
+
capture_output=True,
|
| 794 |
+
text=True,
|
| 795 |
+
timeout=60
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
if result.returncode != 0:
|
| 799 |
+
return {"success": False, "error": "Failed to take screenshot"}
|
| 800 |
+
|
| 801 |
+
return {
|
| 802 |
+
"success": True,
|
| 803 |
+
"url": url,
|
| 804 |
+
"destination": destination
|
| 805 |
+
}
|
| 806 |
+
except Exception as e:
|
| 807 |
+
return {"success": False, "error": str(e)}
|
| 808 |
+
|
| 809 |
+
|
| 810 |
+
# ============================================================================
|
| 811 |
+
# MEMORY TOOLS (Tools 28-32)
|
| 812 |
+
# ============================================================================
|
| 813 |
+
|
| 814 |
+
def tool_memory_recall(query: str, max_results: int = 5) -> Dict[str, Any]:
|
| 815 |
+
"""Recall from memory (searches memory files)."""
|
| 816 |
+
try:
|
| 817 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 818 |
+
|
| 819 |
+
# Search in memory files
|
| 820 |
+
results = []
|
| 821 |
+
|
| 822 |
+
# Search MEMORY.md
|
| 823 |
+
memory_file = workspace / "MEMORY.md"
|
| 824 |
+
if memory_file.exists():
|
| 825 |
+
content = memory_file.read_text()
|
| 826 |
+
if query.lower() in content.lower():
|
| 827 |
+
results.append(str(memory_file))
|
| 828 |
+
|
| 829 |
+
# Search memory folder
|
| 830 |
+
memory_dir = workspace / "memory"
|
| 831 |
+
if memory_dir.exists():
|
| 832 |
+
for f in memory_dir.rglob("*.md"):
|
| 833 |
+
try:
|
| 834 |
+
content = f.read_text()
|
| 835 |
+
if query.lower() in content.lower():
|
| 836 |
+
results.append(str(f))
|
| 837 |
+
except:
|
| 838 |
+
continue
|
| 839 |
+
|
| 840 |
+
return {
|
| 841 |
+
"success": True,
|
| 842 |
+
"query": query,
|
| 843 |
+
"matches": results[:max_results],
|
| 844 |
+
"count": len(results)
|
| 845 |
+
}
|
| 846 |
+
except Exception as e:
|
| 847 |
+
return {"success": False, "error": str(e)}
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
def tool_memory_save(key: str, value: str) -> Dict[str, Any]:
|
| 851 |
+
"""Save to memory."""
|
| 852 |
+
try:
|
| 853 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 854 |
+
memory_file = workspace / "MEMORY.md"
|
| 855 |
+
|
| 856 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 857 |
+
entry = f"\n### {key}\n_{timestamp}_\n{value}\n"
|
| 858 |
+
|
| 859 |
+
with open(memory_file, "a") as f:
|
| 860 |
+
f.write(entry)
|
| 861 |
+
|
| 862 |
+
return {
|
| 863 |
+
"success": True,
|
| 864 |
+
"key": key,
|
| 865 |
+
"saved": True
|
| 866 |
+
}
|
| 867 |
+
except Exception as e:
|
| 868 |
+
return {"success": False, "error": str(e)}
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
def tool_memory_list() -> Dict[str, Any]:
|
| 872 |
+
"""List memory entries."""
|
| 873 |
+
try:
|
| 874 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 875 |
+
memory_file = workspace / "MEMORY.md"
|
| 876 |
+
|
| 877 |
+
if not memory_file.exists():
|
| 878 |
+
return {"success": True, "entries": []}
|
| 879 |
+
|
| 880 |
+
content = memory_file.read_text()
|
| 881 |
+
|
| 882 |
+
# Extract sections
|
| 883 |
+
pattern = r"### (.+?)\n.*?\n(.*?)(?=### |$)"
|
| 884 |
+
matches = re.findall(pattern, content, re.DOTALL)
|
| 885 |
+
|
| 886 |
+
entries = [{"title": m[0].strip(), "content": m[1].strip()[:200]} for m in matches]
|
| 887 |
+
|
| 888 |
+
return {
|
| 889 |
+
"success": True,
|
| 890 |
+
"entries": entries,
|
| 891 |
+
"count": len(entries)
|
| 892 |
+
}
|
| 893 |
+
except Exception as e:
|
| 894 |
+
return {"success": False, "error": str(e)}
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
def tool_context_load(projects: Optional[List[str]] = None) -> Dict[str, Any]:
|
| 898 |
+
"""Load project context."""
|
| 899 |
+
try:
|
| 900 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 901 |
+
|
| 902 |
+
context = {}
|
| 903 |
+
|
| 904 |
+
# Load AGENTS.md
|
| 905 |
+
agents_file = workspace / "AGENTS.md"
|
| 906 |
+
if agents_file.exists():
|
| 907 |
+
context["agents"] = agents_file.read_text()
|
| 908 |
+
|
| 909 |
+
# Load SOUL.md
|
| 910 |
+
soul_file = workspace / "SOUL.md"
|
| 911 |
+
if soul_file.exists():
|
| 912 |
+
context["soul"] = soul_file.read_text()
|
| 913 |
+
|
| 914 |
+
# Load TOOLS.md
|
| 915 |
+
tools_file = workspace / "TOOLS.md"
|
| 916 |
+
if tools_file.exists():
|
| 917 |
+
context["tools"] = tools_file.read_text()
|
| 918 |
+
|
| 919 |
+
return {
|
| 920 |
+
"success": True,
|
| 921 |
+
"context": context,
|
| 922 |
+
"loaded": list(context.keys())
|
| 923 |
+
}
|
| 924 |
+
except Exception as e:
|
| 925 |
+
return {"success": False, "error": str(e)}
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
def tool_project_scan(path: str = ".") -> Dict[str, Any]:
|
| 929 |
+
"""Scan project structure."""
|
| 930 |
+
try:
|
| 931 |
+
base = Path(path)
|
| 932 |
+
|
| 933 |
+
if not base.exists():
|
| 934 |
+
return {"success": False, "error": f"Path not found: {path}"}
|
| 935 |
+
|
| 936 |
+
info = {
|
| 937 |
+
"name": base.name,
|
| 938 |
+
"files": [],
|
| 939 |
+
"dirs": [],
|
| 940 |
+
"has_git": (base / ".git").exists(),
|
| 941 |
+
"has_pyproject": (base / "pyproject.toml").exists(),
|
| 942 |
+
"has_package_json": (base / "package.json").exists(),
|
| 943 |
+
"has_dockerfile": (base / "Dockerfile").exists()
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
for item in base.rglob("*"):
|
| 947 |
+
if len(info["files"]) + len(info["dirs"]) > 100:
|
| 948 |
+
break
|
| 949 |
+
|
| 950 |
+
rel = item.relative_to(base)
|
| 951 |
+
if item.is_dir():
|
| 952 |
+
info["dirs"].append(str(rel))
|
| 953 |
+
else:
|
| 954 |
+
info["files"].append(str(rel))
|
| 955 |
+
|
| 956 |
+
return {
|
| 957 |
+
"success": True,
|
| 958 |
+
"project": info
|
| 959 |
+
}
|
| 960 |
+
except Exception as e:
|
| 961 |
+
return {"success": False, "error": str(e)}
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
# ============================================================================
|
| 965 |
+
# TASK PLANNING TOOLS (Tools 33-37)
|
| 966 |
+
# ============================================================================
|
| 967 |
+
|
| 968 |
+
def tool_create_task(title: str, description: str = "", priority: str = "medium") -> Dict[str, Any]:
|
| 969 |
+
"""Create a task."""
|
| 970 |
+
try:
|
| 971 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 972 |
+
tasks_file = workspace / ".tasks.json"
|
| 973 |
+
|
| 974 |
+
tasks = []
|
| 975 |
+
if tasks_file.exists():
|
| 976 |
+
tasks = json.loads(tasks_file.read_text())
|
| 977 |
+
|
| 978 |
+
task_id = hashlib.md5(f"{title}{datetime.now()}".encode()).hexdigest()[:8]
|
| 979 |
+
|
| 980 |
+
task = {
|
| 981 |
+
"id": task_id,
|
| 982 |
+
"title": title,
|
| 983 |
+
"description": description,
|
| 984 |
+
"priority": priority,
|
| 985 |
+
"status": "pending",
|
| 986 |
+
"created": datetime.now().isoformat()
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
tasks.append(task)
|
| 990 |
+
tasks_file.write_text(json.dumps(tasks, indent=2))
|
| 991 |
+
|
| 992 |
+
return {
|
| 993 |
+
"success": True,
|
| 994 |
+
"task": task
|
| 995 |
+
}
|
| 996 |
+
except Exception as e:
|
| 997 |
+
return {"success": False, "error": str(e)}
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
def tool_list_tasks(status: Optional[str] = None, priority: Optional[str] = None) -> Dict[str, Any]:
|
| 1001 |
+
"""List tasks."""
|
| 1002 |
+
try:
|
| 1003 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 1004 |
+
tasks_file = workspace / ".tasks.json"
|
| 1005 |
+
|
| 1006 |
+
if not tasks_file.exists():
|
| 1007 |
+
return {"success": True, "tasks": []}
|
| 1008 |
+
|
| 1009 |
+
tasks = json.loads(tasks_file.read_text())
|
| 1010 |
+
|
| 1011 |
+
if status:
|
| 1012 |
+
tasks = [t for t in tasks if t.get("status") == status]
|
| 1013 |
+
if priority:
|
| 1014 |
+
tasks = [t for t in tasks if t.get("priority") == priority]
|
| 1015 |
+
|
| 1016 |
+
return {
|
| 1017 |
+
"success": True,
|
| 1018 |
+
"tasks": tasks,
|
| 1019 |
+
"count": len(tasks)
|
| 1020 |
+
}
|
| 1021 |
+
except Exception as e:
|
| 1022 |
+
return {"success": False, "error": str(e)}
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
def tool_update_task(task_id: str, status: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
| 1026 |
+
"""Update a task."""
|
| 1027 |
+
try:
|
| 1028 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 1029 |
+
tasks_file = workspace / ".tasks.json"
|
| 1030 |
+
|
| 1031 |
+
if not tasks_file.exists():
|
| 1032 |
+
return {"success": False, "error": "No tasks found"}
|
| 1033 |
+
|
| 1034 |
+
tasks = json.loads(tasks_file.read_text())
|
| 1035 |
+
|
| 1036 |
+
for task in tasks:
|
| 1037 |
+
if task.get("id") == task_id:
|
| 1038 |
+
if status:
|
| 1039 |
+
task["status"] = status
|
| 1040 |
+
task.update(kwargs)
|
| 1041 |
+
task["updated"] = datetime.now().isoformat()
|
| 1042 |
+
break
|
| 1043 |
+
|
| 1044 |
+
tasks_file.write_text(json.dumps(tasks, indent=2))
|
| 1045 |
+
|
| 1046 |
+
return {
|
| 1047 |
+
"success": True,
|
| 1048 |
+
"task_id": task_id,
|
| 1049 |
+
"updated": True
|
| 1050 |
+
}
|
| 1051 |
+
except Exception as e:
|
| 1052 |
+
return {"success": False, "error": str(e)}
|
| 1053 |
+
|
| 1054 |
+
|
| 1055 |
+
def tool_delete_task(task_id: str) -> Dict[str, Any]:
|
| 1056 |
+
"""Delete a task."""
|
| 1057 |
+
try:
|
| 1058 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 1059 |
+
tasks_file = workspace / ".tasks.json"
|
| 1060 |
+
|
| 1061 |
+
if not tasks_file.exists():
|
| 1062 |
+
return {"success": False, "error": "No tasks found"}
|
| 1063 |
+
|
| 1064 |
+
tasks = json.loads(tasks_file.read_text())
|
| 1065 |
+
tasks = [t for t in tasks if t.get("id") != task_id]
|
| 1066 |
+
|
| 1067 |
+
tasks_file.write_text(json.dumps(tasks, indent=2))
|
| 1068 |
+
|
| 1069 |
+
return {
|
| 1070 |
+
"success": True,
|
| 1071 |
+
"task_id": task_id,
|
| 1072 |
+
"deleted": True
|
| 1073 |
+
}
|
| 1074 |
+
except Exception as e:
|
| 1075 |
+
return {"success": False, "error": str(e)}
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def tool_create_plan(goal: str, steps: List[str]) -> Dict[str, Any]:
|
| 1079 |
+
"""Create an execution plan."""
|
| 1080 |
+
try:
|
| 1081 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 1082 |
+
plans_file = workspace / ".plans.json"
|
| 1083 |
+
|
| 1084 |
+
plans = []
|
| 1085 |
+
if plans_file.exists():
|
| 1086 |
+
plans = json.loads(plans_file.read_text())
|
| 1087 |
+
|
| 1088 |
+
plan_id = hashlib.md5(f"{goal}{datetime.now()}".encode()).hexdigest()[:8]
|
| 1089 |
+
|
| 1090 |
+
plan = {
|
| 1091 |
+
"id": plan_id,
|
| 1092 |
+
"goal": goal,
|
| 1093 |
+
"steps": steps,
|
| 1094 |
+
"status": "pending",
|
| 1095 |
+
"created": datetime.now().isoformat()
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
plans.append(plan)
|
| 1099 |
+
plans_file.write_text(json.dumps(plans, indent=2))
|
| 1100 |
+
|
| 1101 |
+
return {
|
| 1102 |
+
"success": True,
|
| 1103 |
+
"plan": plan
|
| 1104 |
+
}
|
| 1105 |
+
except Exception as e:
|
| 1106 |
+
return {"success": False, "error": str(e)}
|
| 1107 |
+
|
| 1108 |
+
|
| 1109 |
+
def tool_execute_plan(plan_id: str) -> Dict[str, Any]:
|
| 1110 |
+
"""Execute a plan step by step."""
|
| 1111 |
+
try:
|
| 1112 |
+
workspace = Path("/Users/walidsobhi/.openclaw/workspace")
|
| 1113 |
+
plans_file = workspace / ".plans.json"
|
| 1114 |
+
|
| 1115 |
+
if not plans_file.exists():
|
| 1116 |
+
return {"success": False, "error": "No plans found"}
|
| 1117 |
+
|
| 1118 |
+
plans = json.loads(plans_file.read_text())
|
| 1119 |
+
|
| 1120 |
+
for plan in plans:
|
| 1121 |
+
if plan.get("id") == plan_id:
|
| 1122 |
+
plan["status"] = "in_progress"
|
| 1123 |
+
plan["started"] = datetime.now().isoformat()
|
| 1124 |
+
break
|
| 1125 |
+
|
| 1126 |
+
plans_file.write_text(json.dumps(plans, indent=2))
|
| 1127 |
+
|
| 1128 |
+
return {
|
| 1129 |
+
"success": True,
|
| 1130 |
+
"plan_id": plan_id,
|
| 1131 |
+
"status": "executing",
|
| 1132 |
+
"steps": plan.get("steps", [])
|
| 1133 |
+
}
|
| 1134 |
+
except Exception as e:
|
| 1135 |
+
return {"success": False, "error": str(e)}
|
| 1136 |
+
|
| 1137 |
+
|
| 1138 |
+
# ============================================================================
|
| 1139 |
+
# TOOL REGISTRY
|
| 1140 |
+
# ============================================================================
|
| 1141 |
+
|
| 1142 |
+
TOOLS: Dict[str, Callable] = {
|
| 1143 |
+
# File operations (1-8)
|
| 1144 |
+
"read": tool_read_file,
|
| 1145 |
+
"write": tool_write_file,
|
| 1146 |
+
"edit": tool_edit_file,
|
| 1147 |
+
"search": tool_search_files,
|
| 1148 |
+
"grep": tool_grep,
|
| 1149 |
+
"copy": tool_copy_file,
|
| 1150 |
+
"move": tool_move_file,
|
| 1151 |
+
"delete": tool_delete_file,
|
| 1152 |
+
|
| 1153 |
+
# Git operations (9-15)
|
| 1154 |
+
"git_status": tool_git_status,
|
| 1155 |
+
"git_commit": tool_git_commit,
|
| 1156 |
+
"git_push": tool_git_push,
|
| 1157 |
+
"git_pull": tool_git_pull,
|
| 1158 |
+
"git_branch": tool_git_branch,
|
| 1159 |
+
"git_log": tool_git_log,
|
| 1160 |
+
"git_diff": tool_git_diff,
|
| 1161 |
+
|
| 1162 |
+
# Code execution (16-22)
|
| 1163 |
+
"run": tool_run_command,
|
| 1164 |
+
"test": tool_run_tests,
|
| 1165 |
+
"lint": tool_lint_code,
|
| 1166 |
+
"format": tool_format_code,
|
| 1167 |
+
"typecheck": tool_check_type,
|
| 1168 |
+
"server": tool_start_server,
|
| 1169 |
+
"install": tool_install_dependencies,
|
| 1170 |
+
|
| 1171 |
+
# Web (23-27)
|
| 1172 |
+
"web_search": tool_web_search,
|
| 1173 |
+
"fetch": tool_web_fetch,
|
| 1174 |
+
"download": tool_download_file,
|
| 1175 |
+
"check_url": tool_check_url,
|
| 1176 |
+
"screenshot": tool_screenshot,
|
| 1177 |
+
|
| 1178 |
+
# Memory (28-32)
|
| 1179 |
+
"memory_recall": tool_memory_recall,
|
| 1180 |
+
"memory_save": tool_memory_save,
|
| 1181 |
+
"memory_list": tool_memory_list,
|
| 1182 |
+
"context_load": tool_context_load,
|
| 1183 |
+
"project_scan": tool_project_scan,
|
| 1184 |
+
|
| 1185 |
+
# Task planning (33-37)
|
| 1186 |
+
"create_task": tool_create_task,
|
| 1187 |
+
"list_tasks": tool_list_tasks,
|
| 1188 |
+
"update_task": tool_update_task,
|
| 1189 |
+
"delete_task": tool_delete_task,
|
| 1190 |
+
"create_plan": tool_create_plan,
|
| 1191 |
+
"execute_plan": tool_execute_plan,
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
|
| 1195 |
+
def get_tool(name: str) -> Optional[Callable]:
|
| 1196 |
+
"""Get tool by name."""
|
| 1197 |
+
return TOOLS.get(name)
|
| 1198 |
+
|
| 1199 |
+
|
| 1200 |
+
def list_tools() -> List[str]:
|
| 1201 |
+
"""List all available tools."""
|
| 1202 |
+
return list(TOOLS.keys())
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
def get_tool_schemas() -> List[Dict[str, Any]]:
|
| 1206 |
+
"""Get tool schemas for LLM tool calling.
|
| 1207 |
+
|
| 1208 |
+
Automatically generates JSON Schema from function signatures using inspect.
|
| 1209 |
+
All 38 tools are included with accurate parameter types and descriptions.
|
| 1210 |
+
"""
|
| 1211 |
+
import inspect
|
| 1212 |
+
from typing import get_type_hints
|
| 1213 |
+
|
| 1214 |
+
schemas = []
|
| 1215 |
+
|
| 1216 |
+
for name, func in TOOLS.items():
|
| 1217 |
+
sig = inspect.signature(func)
|
| 1218 |
+
doc = func.__doc__ or f"Tool: {name}"
|
| 1219 |
+
|
| 1220 |
+
# Build parameters schema
|
| 1221 |
+
properties = {}
|
| 1222 |
+
required = []
|
| 1223 |
+
|
| 1224 |
+
for param_name, param in sig.parameters.items():
|
| 1225 |
+
# Skip self/cls
|
| 1226 |
+
if param_name in ('self', 'cls'):
|
| 1227 |
+
continue
|
| 1228 |
+
|
| 1229 |
+
# Get type annotation
|
| 1230 |
+
annotation = param.annotation
|
| 1231 |
+
if annotation is inspect.Parameter.empty:
|
| 1232 |
+
json_type = "string" # default
|
| 1233 |
+
elif annotation is str:
|
| 1234 |
+
json_type = "string"
|
| 1235 |
+
elif annotation is int:
|
| 1236 |
+
json_type = "integer"
|
| 1237 |
+
elif annotation is bool:
|
| 1238 |
+
json_type = "boolean"
|
| 1239 |
+
elif annotation is float:
|
| 1240 |
+
json_type = "number"
|
| 1241 |
+
elif hasattr(annotation, '__origin__') and annotation.__origin__ is list:
|
| 1242 |
+
json_type = "array"
|
| 1243 |
+
elif hasattr(annotation, '__origin__') and annotation.__origin__ is dict:
|
| 1244 |
+
json_type = "object"
|
| 1245 |
+
else:
|
| 1246 |
+
json_type = "string" # fallback
|
| 1247 |
+
|
| 1248 |
+
# Build property definition
|
| 1249 |
+
prop = {"type": json_type}
|
| 1250 |
+
|
| 1251 |
+
# Extract description from docstring
|
| 1252 |
+
param_desc = _extract_param_desc(doc, param_name)
|
| 1253 |
+
if param_desc:
|
| 1254 |
+
prop["description"] = param_desc
|
| 1255 |
+
|
| 1256 |
+
# Add enum for restricted string values
|
| 1257 |
+
if param_name in ('linter', 'formatter', 'package_manager') and hasattr(annotation, '__args__'):
|
| 1258 |
+
prop["enum"] = list(annotation.__args__)
|
| 1259 |
+
|
| 1260 |
+
properties[param_name] = prop
|
| 1261 |
+
|
| 1262 |
+
# Mark as required if no default value
|
| 1263 |
+
if param.default is inspect.Parameter.empty:
|
| 1264 |
+
required.append(param_name)
|
| 1265 |
+
|
| 1266 |
+
schema = {
|
| 1267 |
+
"name": name,
|
| 1268 |
+
"description": doc.strip().split('\n')[0],
|
| 1269 |
+
"parameters": {
|
| 1270 |
+
"type": "object",
|
| 1271 |
+
"properties": properties,
|
| 1272 |
+
"required": required
|
| 1273 |
+
}
|
| 1274 |
+
}
|
| 1275 |
+
|
| 1276 |
+
schemas.append(schema)
|
| 1277 |
+
|
| 1278 |
+
return schemas
|
| 1279 |
+
|
| 1280 |
+
|
| 1281 |
+
def _extract_param_desc(docstring: str, param_name: str) -> Optional[str]:
|
| 1282 |
+
"""Extract parameter description from docstring.
|
| 1283 |
+
|
| 1284 |
+
Looks for lines like: "- `param_name`: description" or "param_name: description".
|
| 1285 |
+
"""
|
| 1286 |
+
if not docstring:
|
| 1287 |
+
return None
|
| 1288 |
+
|
| 1289 |
+
lines = docstring.split('\n')
|
| 1290 |
+
for i, line in enumerate(lines):
|
| 1291 |
+
# Match: - `param`: description
|
| 1292 |
+
if f"`{param_name}`" in line or f"{param_name}:" in line:
|
| 1293 |
+
# Try to extract after colon or dash
|
| 1294 |
+
parts = line.split(':', 1)
|
| 1295 |
+
if len(parts) > 1:
|
| 1296 |
+
return parts[1].strip().lstrip(' -').strip()
|
| 1297 |
+
# Alternative: split on backtick
|
| 1298 |
+
parts = line.split('`', 2)
|
| 1299 |
+
if len(parts) > 2:
|
| 1300 |
+
return parts[2].strip().lstrip(': -').strip()
|
| 1301 |
+
|
| 1302 |
+
return None
|
| 1303 |
+
|
| 1304 |
+
|
| 1305 |
+
if __name__ == "__main__":
|
| 1306 |
+
print("Stack 2.9 Tools Module")
|
| 1307 |
+
print(f"Available tools: {len(TOOLS)}")
|
| 1308 |
+
print(list_tools())
|
audit_report.txt → audits/audit_report.txt
RENAMED
|
File without changes
|
audit_results.json → audits/audit_results.json
RENAMED
|
File without changes
|
Dockerfile → config/Dockerfile
RENAMED
|
File without changes
|
Dockerfile.gpu → config/Dockerfile.gpu
RENAMED
|
File without changes
|
MLproject → config/MLproject
RENAMED
|
File without changes
|
Makefile → config/Makefile
RENAMED
|
File without changes
|
docker-compose.gpu.yml → config/docker-compose.gpu.yml
RENAMED
|
File without changes
|
{k8s → config/k8s}/deployment.yaml
RENAMED
|
File without changes
|
{k8s → config/k8s}/pvc.yaml
RENAMED
|
File without changes
|
{k8s → config/k8s}/secret.yaml
RENAMED
|
File without changes
|
{k8s → config/k8s}/service.yaml
RENAMED
|
File without changes
|
package-lock.json → config/package-lock.json
RENAMED
|
File without changes
|
package.json → config/package.json
RENAMED
|
File without changes
|
pyproject.toml → config/pyproject.toml
RENAMED
|
File without changes
|
tsconfig.json → config/tsconfig.json
RENAMED
|
File without changes
|
CHANGELOG.md → docs/CHANGELOG.md
RENAMED
|
File without changes
|
CODE_OF_CONDUCT.md → docs/CODE_OF_CONDUCT.md
RENAMED
|
File without changes
|
CONTRIBUTING.md → docs/CONTRIBUTING.md
RENAMED
|
File without changes
|
DIRECTORY_STRUCTURE.md → docs/DIRECTORY_STRUCTURE.md
RENAMED
|
File without changes
|
GIT_PUSH.md → docs/GIT_PUSH.md
RENAMED
|
File without changes
|
LAUNCH_CHECKLIST.md → docs/LAUNCH_CHECKLIST.md
RENAMED
|
File without changes
|
LAUNCH_PLAN.md → docs/LAUNCH_PLAN.md
RENAMED
|
File without changes
|
LICENSE → docs/LICENSE
RENAMED
|
File without changes
|
MODEL_CARD.md → docs/MODEL_CARD.md
RENAMED
|
File without changes
|
MODEL_REGISTRY.md → docs/MODEL_REGISTRY.md
RENAMED
|
File without changes
|
README.md → docs/README.md
RENAMED
|
@@ -112,6 +112,30 @@ result = await registry.call("grep", {"pattern": "def main", "path": "./src"})
|
|
| 112 |
|
| 113 |
---
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
## 🛠️ Full Tool List (57 Tools)
|
| 116 |
|
| 117 |
### File Operations (5)
|
|
|
|
| 112 |
|
| 113 |
---
|
| 114 |
|
| 115 |
+
## 🔌 OpenClaw Integration
|
| 116 |
+
|
| 117 |
+
Stack 2.9 ships as an **MCP server**, exposing all 69 tools to [OpenClaw](https://github.com/openclaw) for seamless integration.
|
| 118 |
+
|
| 119 |
+
### Register the MCP Server
|
| 120 |
+
|
| 121 |
+
If not already configured, add it to OpenClaw:
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
openclaw mcp set Stack2.9 '{"command":"python3","args":["src/mcp_server.py"],"cwd":"~/stack-2.9"}'
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
### Start the MCP Server
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
cd ~/stack-2.9 && PYTHONPATH=. python3 src/mcp_server.py
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Use from OpenClaw
|
| 134 |
+
|
| 135 |
+
Once registered, OpenClaw can directly call any tool: `file_read`, `grep`, `task_create`, `web_search`, `mcp_call`, and 65 more — no local GPU needed for the model.
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
## 🛠️ Full Tool List (57 Tools)
|
| 140 |
|
| 141 |
### File Operations (5)
|
README_QUICKSTART.md → docs/README_QUICKSTART.md
RENAMED
|
File without changes
|
SECURITY.md → docs/SECURITY.md
RENAMED
|
File without changes
|
docs/tools.md
CHANGED
|
@@ -1,162 +1,40 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
##
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- A JSON-schema `input_schema`
|
| 9 |
-
- An `execute(input)` method returning `ToolResult[T]`
|
| 10 |
-
- Optional `validate_input()` for pre-execution checks
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
##
|
| 15 |
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
from src.tools.web_search import WebSearchTool
|
| 22 |
-
tool = WebSearchTool()
|
| 23 |
-
result = tool.call({
|
| 24 |
-
"query": "latest AI news",
|
| 25 |
-
"max_results": 10,
|
| 26 |
-
"allowed_domains": None,
|
| 27 |
-
"blocked_domains": None,
|
| 28 |
-
})
|
| 29 |
-
```
|
| 30 |
-
|
| 31 |
-
**Parameters:**
|
| 32 |
-
| Name | Type | Required | Description |
|
| 33 |
-
|------|------|----------|-------------|
|
| 34 |
-
| `query` | string | Yes | Search query (min 2 chars) |
|
| 35 |
-
| `allowed_domains` | string[] | No | Restrict to domains |
|
| 36 |
-
| `blocked_domains` | string[] | No | Exclude domains |
|
| 37 |
-
| `max_results` | int | No | Max results (default 10, max 20) |
|
| 38 |
-
|
| 39 |
-
**Output:**
|
| 40 |
-
```json
|
| 41 |
-
{
|
| 42 |
-
"query": "...",
|
| 43 |
-
"results": [{"title": "...", "url": "...", "snippet": "..."}],
|
| 44 |
-
"duration_seconds": 0.5,
|
| 45 |
-
"source": "duckduckgo"
|
| 46 |
-
}
|
| 47 |
-
```
|
| 48 |
-
|
| 49 |
-
---
|
| 50 |
-
|
| 51 |
-
### Task Management
|
| 52 |
-
|
| 53 |
-
Four tools for task lifecycle management. Tasks stored in `~/.stack-2.9/tasks.json`.
|
| 54 |
-
|
| 55 |
-
#### TaskCreate
|
| 56 |
-
|
| 57 |
-
```python
|
| 58 |
-
result = get_registry().call("TaskCreate", {
|
| 59 |
-
"subject": "Fix login bug",
|
| 60 |
-
"description": "Users cannot log in with SSO",
|
| 61 |
-
"status": "pending",
|
| 62 |
-
"priority": "high",
|
| 63 |
-
"tags": ["bug", "auth"],
|
| 64 |
-
})
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
#### TaskList
|
| 68 |
-
|
| 69 |
-
```python
|
| 70 |
-
result = get_registry().call("TaskList", {
|
| 71 |
-
"status": "pending",
|
| 72 |
-
"tag": "bug",
|
| 73 |
-
"limit": 50,
|
| 74 |
-
})
|
| 75 |
-
```
|
| 76 |
-
|
| 77 |
-
#### TaskUpdate
|
| 78 |
-
|
| 79 |
-
```python
|
| 80 |
-
result = get_registry().call("TaskUpdate", {
|
| 81 |
-
"id": "a1b2c3d4",
|
| 82 |
-
"status": "completed",
|
| 83 |
-
})
|
| 84 |
-
```
|
| 85 |
-
|
| 86 |
-
#### TaskDelete
|
| 87 |
-
|
| 88 |
-
```python
|
| 89 |
-
result = get_registry().call("TaskDelete", {"id": "a1b2c3d4"})
|
| 90 |
-
```
|
| 91 |
-
|
| 92 |
-
**Task Status Values:** `pending`, `in_progress`, `completed`, `cancelled`
|
| 93 |
-
|
| 94 |
-
**Priority Values:** `low`, `medium`, `high`, `urgent`
|
| 95 |
-
|
| 96 |
-
---
|
| 97 |
|
| 98 |
-
|
| 99 |
|
| 100 |
-
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
#### CronCreate
|
| 105 |
-
|
| 106 |
-
```python
|
| 107 |
-
result = get_registry().call("CronCreate", {
|
| 108 |
-
"cron": "*/5 * * * *", # every 5 minutes
|
| 109 |
-
"prompt": "Check system status",
|
| 110 |
-
"recurring": True,
|
| 111 |
-
"durable": True, # persists across restarts
|
| 112 |
-
})
|
| 113 |
```
|
| 114 |
|
| 115 |
-
|
| 116 |
-
| Expression | Meaning |
|
| 117 |
-
|------------|---------|
|
| 118 |
-
| `*/5 * * * *` | Every 5 minutes |
|
| 119 |
-
| `0 * * * *` | Every hour |
|
| 120 |
-
| `0 9 * * 1-5` | Weekdays at 9am |
|
| 121 |
-
| `30 14 * * *` | 2:30pm daily |
|
| 122 |
-
| `0 0 1 * *` | Midnight on 1st of month |
|
| 123 |
-
|
| 124 |
-
#### CronList
|
| 125 |
-
|
| 126 |
-
```python
|
| 127 |
-
result = get_registry().call("CronList", {})
|
| 128 |
-
```
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
```python
|
| 133 |
-
result = get_registry().call("CronDelete", {"id": "schedule-id"})
|
| 134 |
-
```
|
| 135 |
|
| 136 |
---
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
```python
|
| 141 |
-
from src.tools import get_registry
|
| 142 |
-
|
| 143 |
-
registry = get_registry()
|
| 144 |
-
registry.list() # list all tool names
|
| 145 |
-
registry.get("WebSearch") # get a specific tool
|
| 146 |
-
registry.call("TaskCreate", {...}) # validate + execute in one step
|
| 147 |
-
```
|
| 148 |
-
|
| 149 |
-
## Adding New Tools
|
| 150 |
-
|
| 151 |
-
1. Create `src/tools/your_tool.py` extending `BaseTool`
|
| 152 |
-
2. Define `name`, `description`, `input_schema`, and `execute()`
|
| 153 |
-
3. Import and register at the bottom of the file: `get_registry().register(YourTool())`
|
| 154 |
-
4. Add tests in `tests/test_your_tool.py`
|
| 155 |
-
5. Document in this file
|
| 156 |
-
|
| 157 |
-
## Data Storage
|
| 158 |
-
|
| 159 |
-
All tools store data under `~/.stack-2.9/`:
|
| 160 |
-
- `tasks.json` — task list
|
| 161 |
-
- `schedules.json` — cron schedules
|
| 162 |
-
- `web_search_cache.json` — cached search results
|
|
|
|
| 1 |
+
# TOOLS.md - Local Notes
|
| 2 |
|
| 3 |
+
Skills define _how_ tools work. This file is for _your_ specifics — the stuff that's unique to your setup.
|
| 4 |
|
| 5 |
+
## What Goes Here
|
| 6 |
|
| 7 |
+
Things like:
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
- Camera names and locations
|
| 10 |
+
- SSH hosts and aliases
|
| 11 |
+
- Preferred voices for TTS
|
| 12 |
+
- Speaker/room names
|
| 13 |
+
- Device nicknames
|
| 14 |
+
- Anything environment-specific
|
| 15 |
|
| 16 |
+
## Examples
|
| 17 |
|
| 18 |
+
```markdown
|
| 19 |
+
### Cameras
|
| 20 |
|
| 21 |
+
- living-room → Main area, 180° wide angle
|
| 22 |
+
- front-door → Entrance, motion-triggered
|
| 23 |
|
| 24 |
+
### SSH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
- home-server → 192.168.1.100, user: admin
|
| 27 |
|
| 28 |
+
### TTS
|
| 29 |
|
| 30 |
+
- Preferred voice: "Nova" (warm, slightly British)
|
| 31 |
+
- Default speaker: Kitchen HomePod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
```
|
| 33 |
|
| 34 |
+
## Why Separate?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
---
|
| 39 |
|
| 40 |
+
Add whatever helps you do your job. This is your cheat sheet.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
colab_train_stack29.ipynb → notebooks/colab_train_stack29.ipynb
RENAMED
|
File without changes
|
kaggle_train_stack29_v5.ipynb → notebooks/kaggle_train_stack29_v5.ipynb
RENAMED
|
File without changes
|
requirements.txt → requirements/requirements.txt
RENAMED
|
File without changes
|
requirements_api.txt → requirements/requirements_api.txt
RENAMED
|
File without changes
|
requirements_webui.txt → requirements/requirements_webui.txt
RENAMED
|
File without changes
|
install.sh → scripts/deploy/install.sh
RENAMED
|
File without changes
|
runpod_deploy.sh → scripts/deploy/runpod_deploy.sh
RENAMED
|
File without changes
|
setup.sh → scripts/deploy/setup.sh
RENAMED
|
File without changes
|
vastai_deploy.sh → scripts/deploy/vastai_deploy.sh
RENAMED
|
File without changes
|
src/mcp_server.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
| 1 |
"""MCP Server for Stack 2.9 - Exposes Stack tools via Model Context Protocol"""
|
| 2 |
|
| 3 |
import asyncio
|
|
|
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from mcp.server.fastmcp import FastMCP
|
| 7 |
|
| 8 |
# Import all Stack 2.9 tools (triggers auto-registration)
|
|
|
|
| 1 |
"""MCP Server for Stack 2.9 - Exposes Stack tools via Model Context Protocol"""
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
+
# Ensure project root is on the path so 'from src.tools import' works
|
| 9 |
+
_project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
+
if _project_root not in sys.path:
|
| 11 |
+
sys.path.insert(0, _project_root)
|
| 12 |
+
|
| 13 |
from mcp.server.fastmcp import FastMCP
|
| 14 |
|
| 15 |
# Import all Stack 2.9 tools (triggers auto-registration)
|
src/tools/grep_tool.py
CHANGED
|
@@ -29,8 +29,15 @@ class GrepTool(BaseTool):
|
|
| 29 |
"required": ["pattern", "path"]
|
| 30 |
}
|
| 31 |
|
| 32 |
-
|
| 33 |
"""Search for pattern in files."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
search_path = Path(path)
|
| 35 |
if not search_path.exists():
|
| 36 |
return ToolResult(success=False, error=f"Path not found: {path}")
|
|
@@ -135,16 +142,9 @@ class GrepCountTool(BaseTool):
|
|
| 135 |
"required": ["pattern", "path"]
|
| 136 |
}
|
| 137 |
|
| 138 |
-
|
| 139 |
"""Count pattern matches."""
|
| 140 |
-
grep_result =
|
| 141 |
-
pattern=pattern,
|
| 142 |
-
path=path,
|
| 143 |
-
recursive=recursive,
|
| 144 |
-
case_sensitive=case_sensitive,
|
| 145 |
-
file_pattern=file_pattern,
|
| 146 |
-
max_results=100000
|
| 147 |
-
)
|
| 148 |
|
| 149 |
counts = {}
|
| 150 |
for match in grep_result.data.get("matches", []):
|
|
@@ -152,8 +152,8 @@ class GrepCountTool(BaseTool):
|
|
| 152 |
counts[file_path] = counts.get(file_path, 0) + 1
|
| 153 |
|
| 154 |
return ToolResult(success=True, data={
|
| 155 |
-
"pattern": pattern,
|
| 156 |
-
"total_matches": len(
|
| 157 |
"by_file": counts
|
| 158 |
})
|
| 159 |
|
|
|
|
| 29 |
"required": ["pattern", "path"]
|
| 30 |
}
|
| 31 |
|
| 32 |
+
def execute(self, input_data: dict) -> ToolResult:
|
| 33 |
"""Search for pattern in files."""
|
| 34 |
+
pattern = input_data.get('pattern')
|
| 35 |
+
path = input_data.get('path')
|
| 36 |
+
recursive = input_data.get('recursive', True)
|
| 37 |
+
case_sensitive = input_data.get('case_sensitive', True)
|
| 38 |
+
context_lines = input_data.get('context_lines', 0)
|
| 39 |
+
file_pattern = input_data.get('file_pattern')
|
| 40 |
+
max_results = input_data.get('max_results', 1000)
|
| 41 |
search_path = Path(path)
|
| 42 |
if not search_path.exists():
|
| 43 |
return ToolResult(success=False, error=f"Path not found: {path}")
|
|
|
|
| 142 |
"required": ["pattern", "path"]
|
| 143 |
}
|
| 144 |
|
| 145 |
+
def execute(self, input_data: dict) -> ToolResult:
|
| 146 |
"""Count pattern matches."""
|
| 147 |
+
grep_result = GrepTool().execute(input_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
counts = {}
|
| 150 |
for match in grep_result.data.get("matches", []):
|
|
|
|
| 152 |
counts[file_path] = counts.get(file_path, 0) + 1
|
| 153 |
|
| 154 |
return ToolResult(success=True, data={
|
| 155 |
+
"pattern": input_data.get('pattern'),
|
| 156 |
+
"total_matches": len(grep_result.data.get("matches", [])),
|
| 157 |
"by_file": counts
|
| 158 |
})
|
| 159 |
|
stack/training/patterns/feedback.json
CHANGED
|
@@ -1208,5 +1208,25 @@
|
|
| 1208 |
"execution_time": 0.0,
|
| 1209 |
"timestamp": "2026-04-08T17:40:56.111571",
|
| 1210 |
"model_version": null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
}
|
| 1212 |
]
|
|
|
|
| 1208 |
"execution_time": 0.0,
|
| 1209 |
"timestamp": "2026-04-08T17:40:56.111571",
|
| 1210 |
"model_version": null
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"id": "034235bc464e647a",
|
| 1214 |
+
"problem_type": "humaneval",
|
| 1215 |
+
"solution": "accuracy=0.95",
|
| 1216 |
+
"success": true,
|
| 1217 |
+
"error_message": null,
|
| 1218 |
+
"execution_time": 0.0,
|
| 1219 |
+
"timestamp": "2026-04-09T21:51:36.160807",
|
| 1220 |
+
"model_version": null
|
| 1221 |
+
},
|
| 1222 |
+
{
|
| 1223 |
+
"id": "671cde37e210031e",
|
| 1224 |
+
"problem_type": "gsm8k",
|
| 1225 |
+
"solution": "accuracy=0.55",
|
| 1226 |
+
"success": true,
|
| 1227 |
+
"error_message": null,
|
| 1228 |
+
"execution_time": 0.0,
|
| 1229 |
+
"timestamp": "2026-04-09T21:54:33.700093",
|
| 1230 |
+
"model_version": null
|
| 1231 |
}
|
| 1232 |
]
|