mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
Update python monorepo
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ $ pip install -r requirements-dev.lock
|
||||
|
||||
Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
|
||||
result in merge conflicts between manual patches and changes from the generator. The generator will never
|
||||
modify the contents of the `src/hanzoai/lib/` and `examples/` directories.
|
||||
modify the contents of the `pkg/hanzoai/lib/` and `examples/` directories.
|
||||
|
||||
## Adding and running examples
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
.DEFAULT_GOAL := help
|
||||
SHELL := /bin/bash
|
||||
.PHONY: help setup install-python venv deps test lint format build clean publish-all
|
||||
|
||||
# Colors for output
|
||||
CYAN := \033[0;36m
|
||||
GREEN := \033[0;32m
|
||||
RED := \033[0;31m
|
||||
YELLOW := \033[0;33m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
# Python version
|
||||
PYTHON_VERSION := 3.12
|
||||
|
||||
help: ## Show this help message
|
||||
@echo -e "$(CYAN)Hanzo Python SDK Monorepo Makefile$(NC)"
|
||||
@echo -e "$(YELLOW)Usage:$(NC) make [target]"
|
||||
@echo
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-20s$(NC) %s\n", $$1, $$2}'
|
||||
|
||||
setup: install-python venv deps ## Complete setup: install Python, create venv, install deps
|
||||
|
||||
install-python: ## Install Python using uv
|
||||
@echo -e "$(CYAN)Installing Python $(PYTHON_VERSION)...$(NC)"
|
||||
@if ! command -v uv &> /dev/null; then \
|
||||
echo -e "$(RED)Error: uv is not installed. Please install it first.$(NC)"; \
|
||||
echo "Run: curl -LsSf https://astral.sh/uv/install.sh | sh"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@uv python install $(PYTHON_VERSION)
|
||||
@echo -e "$(GREEN)Python $(PYTHON_VERSION) installed successfully$(NC)"
|
||||
|
||||
venv: ## Create virtual environment
|
||||
@echo -e "$(CYAN)Creating virtual environment...$(NC)"
|
||||
@uv venv --python $(PYTHON_VERSION)
|
||||
@echo -e "$(GREEN)Virtual environment created$(NC)"
|
||||
@echo -e "$(YELLOW)Activate with: source .venv/bin/activate$(NC)"
|
||||
|
||||
deps: ## Install all dependencies for all packages
|
||||
@echo -e "$(CYAN)Installing dependencies...$(NC)"
|
||||
@source .venv/bin/activate && uv pip install -e .
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-agents
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-mcp
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-cli
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-repl
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-memory
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-network
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-mcp-client
|
||||
@source .venv/bin/activate && uv pip install -e ./pkg/hanzo-aci
|
||||
@echo -e "$(GREEN)All dependencies installed$(NC)"
|
||||
|
||||
dev-deps: ## Install development dependencies
|
||||
@echo -e "$(CYAN)Installing development dependencies...$(NC)"
|
||||
@source .venv/bin/activate && uv pip install pytest pytest-asyncio mypy ruff black coverage
|
||||
@echo -e "$(GREEN)Development dependencies installed$(NC)"
|
||||
|
||||
test: ## Run tests for all packages
|
||||
@echo -e "$(CYAN)Running tests...$(NC)"
|
||||
@source .venv/bin/activate && python -m pytest tests/ -v
|
||||
@source .venv/bin/activate && cd pkg/hanzo-agents && python -m pytest tests/ -v || true
|
||||
@source .venv/bin/activate && cd pkg/hanzo-mcp && python -m pytest tests/ -v || true
|
||||
@source .venv/bin/activate && cd pkg/hanzo-memory && python -m pytest tests/ -v || true
|
||||
@source .venv/bin/activate && cd pkg/hanzo-aci && python -m pytest tests/ -v || true
|
||||
@echo -e "$(GREEN)Tests completed$(NC)"
|
||||
|
||||
lint: ## Run linting for all packages
|
||||
@echo -e "$(CYAN)Running linters...$(NC)"
|
||||
@source .venv/bin/activate && ruff check . --fix
|
||||
@source .venv/bin/activate && mypy . --ignore-missing-imports || true
|
||||
@echo -e "$(GREEN)Linting completed$(NC)"
|
||||
|
||||
format: ## Format code for all packages
|
||||
@echo -e "$(CYAN)Formatting code...$(NC)"
|
||||
@source .venv/bin/activate && ruff format .
|
||||
@source .venv/bin/activate && black . || true
|
||||
@echo -e "$(GREEN)Formatting completed$(NC)"
|
||||
|
||||
build: ## Build all packages
|
||||
@echo -e "$(CYAN)Building packages...$(NC)"
|
||||
@source .venv/bin/activate && python -m build .
|
||||
@source .venv/bin/activate && cd pkg/hanzo-agents && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-mcp && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-cli && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-repl && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-memory && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-network && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-mcp-client && python -m build
|
||||
@source .venv/bin/activate && cd pkg/hanzo-aci && python -m build
|
||||
@echo -e "$(GREEN)All packages built$(NC)"
|
||||
|
||||
clean: ## Clean build artifacts
|
||||
@echo -e "$(CYAN)Cleaning build artifacts...$(NC)"
|
||||
@find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
@find . -type f -name "*.pyo" -delete 2>/dev/null || true
|
||||
@find . -type f -name ".coverage" -delete 2>/dev/null || true
|
||||
@rm -rf htmlcov/ 2>/dev/null || true
|
||||
@echo -e "$(GREEN)Clean completed$(NC)"
|
||||
|
||||
publish-check: ## Check if packages are ready to publish
|
||||
@echo -e "$(CYAN)Checking package configurations...$(NC)"
|
||||
@python scripts/check_packages.py
|
||||
|
||||
publish-all: build ## Publish all packages to PyPI
|
||||
@echo -e "$(CYAN)Publishing packages to PyPI...$(NC)"
|
||||
@echo -e "$(YELLOW)Warning: This will publish to PyPI. Make sure you have the correct credentials.$(NC)"
|
||||
@read -p "Continue? [y/N] " -n 1 -r; echo; \
|
||||
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
|
||||
$(MAKE) publish-hanzoai; \
|
||||
$(MAKE) publish-hanzo-agents; \
|
||||
$(MAKE) publish-hanzo-mcp; \
|
||||
$(MAKE) publish-hanzo-cli; \
|
||||
$(MAKE) publish-hanzo; \
|
||||
$(MAKE) publish-hanzo-repl; \
|
||||
$(MAKE) publish-hanzo-memory; \
|
||||
$(MAKE) publish-hanzo-network; \
|
||||
$(MAKE) publish-hanzo-mcp-client; \
|
||||
$(MAKE) publish-hanzo-aci; \
|
||||
else \
|
||||
echo -e "$(RED)Publishing cancelled$(NC)"; \
|
||||
fi
|
||||
|
||||
# Publishing individual packages
|
||||
publish-hanzoai: ## Publish hanzoai package
|
||||
@echo -e "$(CYAN)Publishing hanzoai...$(NC)"
|
||||
@cd . && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-agents: ## Publish hanzo-agents package
|
||||
@echo -e "$(CYAN)Publishing hanzo-agents...$(NC)"
|
||||
@cd pkg/hanzo-agents && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-mcp: ## Publish hanzo-mcp package
|
||||
@echo -e "$(CYAN)Publishing hanzo-mcp...$(NC)"
|
||||
@cd pkg/hanzo-mcp && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-cli: ## Publish hanzo-cli package
|
||||
@echo -e "$(CYAN)Publishing hanzo-cli...$(NC)"
|
||||
@cd pkg/hanzo-cli && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo: ## Publish hanzo wrapper package
|
||||
@echo -e "$(CYAN)Building and publishing hanzo wrapper...$(NC)"
|
||||
@cd pkg/hanzo-wrapper && python -m build && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-repl: ## Publish hanzo-repl package
|
||||
@echo -e "$(CYAN)Publishing hanzo-repl...$(NC)"
|
||||
@cd pkg/hanzo-repl && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-memory: ## Publish hanzo-memory package
|
||||
@echo -e "$(CYAN)Publishing hanzo-memory...$(NC)"
|
||||
@cd pkg/hanzo-memory && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-network: ## Publish hanzo-network package
|
||||
@echo -e "$(CYAN)Publishing hanzo-network...$(NC)"
|
||||
@cd pkg/hanzo-network && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-mcp-client: ## Publish hanzo-mcp-client package
|
||||
@echo -e "$(CYAN)Publishing hanzo-mcp-client...$(NC)"
|
||||
@cd pkg/hanzo-mcp-client && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
publish-hanzo-aci: ## Publish hanzo-aci as dev-aci package
|
||||
@echo -e "$(CYAN)Publishing hanzo-aci as dev-aci...$(NC)"
|
||||
@cd pkg/hanzo-aci && TWINE_USERNAME=__token__ TWINE_PASSWORD=$${PYPI_TOKEN} twine upload dist/* --skip-existing
|
||||
|
||||
# Individual package commands
|
||||
test-hanzoai: ## Test main hanzoai package
|
||||
@source .venv/bin/activate && python -m pytest tests/ -v
|
||||
|
||||
test-agents: ## Test hanzo-agents package
|
||||
@source .venv/bin/activate && cd pkg/hanzo-agents && python -m pytest tests/ -v
|
||||
|
||||
test-mcp: ## Test hanzo-mcp package
|
||||
@source .venv/bin/activate && cd pkg/hanzo-mcp && python -m pytest tests/ -v
|
||||
|
||||
test-memory: ## Test hanzo-memory package
|
||||
@source .venv/bin/activate && cd pkg/hanzo-memory && python -m pytest tests/ -v
|
||||
|
||||
test-aci: ## Test hanzo-aci package
|
||||
@source .venv/bin/activate && cd pkg/hanzo-aci && python -m pytest tests/ -v
|
||||
|
||||
# Development helpers
|
||||
shell: ## Start Python shell with packages loaded
|
||||
@source .venv/bin/activate && python
|
||||
|
||||
watch-tests: ## Watch and run tests on file changes
|
||||
@source .venv/bin/activate && watchmedo shell-command \
|
||||
--patterns="*.py" \
|
||||
--recursive \
|
||||
--command='make test' \
|
||||
.
|
||||
|
||||
update-deps: ## Update all dependencies to latest versions
|
||||
@echo -e "$(CYAN)Updating dependencies...$(NC)"
|
||||
@source .venv/bin/activate && uv pip compile pyproject.toml -o requirements.txt --upgrade
|
||||
@source .venv/bin/activate && uv pip sync requirements.txt
|
||||
@echo -e "$(GREEN)Dependencies updated$(NC)"
|
||||
|
||||
check-types: ## Run type checking
|
||||
@echo -e "$(CYAN)Running type checks...$(NC)"
|
||||
@source .venv/bin/activate && mypy pkg/hanzoai --ignore-missing-imports
|
||||
@source .venv/bin/activate && pyright pkg/hanzoai || true
|
||||
@echo -e "$(GREEN)Type checking completed$(NC)"
|
||||
|
||||
coverage: ## Run tests with coverage
|
||||
@echo -e "$(CYAN)Running tests with coverage...$(NC)"
|
||||
@source .venv/bin/activate && coverage run -m pytest tests/
|
||||
@source .venv/bin/activate && coverage report
|
||||
@source .venv/bin/activate && coverage html
|
||||
@echo -e "$(GREEN)Coverage report generated in htmlcov/$(NC)"
|
||||
@@ -27,8 +27,6 @@ from hanzoai import Hanzo
|
||||
|
||||
client = Hanzo(
|
||||
api_key=os.environ.get("HANZO_API_KEY"), # This is the default and can be omitted
|
||||
# defaults to "production".
|
||||
environment="sandbox",
|
||||
)
|
||||
|
||||
response = client.get_home()
|
||||
@@ -50,8 +48,6 @@ from hanzoai import AsyncHanzo
|
||||
|
||||
client = AsyncHanzo(
|
||||
api_key=os.environ.get("HANZO_API_KEY"), # This is the default and can be omitted
|
||||
# defaults to "production".
|
||||
environment="sandbox",
|
||||
)
|
||||
|
||||
|
||||
@@ -83,7 +79,7 @@ from hanzoai import Hanzo
|
||||
client = Hanzo()
|
||||
|
||||
model = client.model.create(
|
||||
llm_params={
|
||||
hanzo_params={
|
||||
"model": "model",
|
||||
"api_base": "api_base",
|
||||
"api_key": "api_key",
|
||||
@@ -96,7 +92,7 @@ model = client.model.create(
|
||||
"custom_llm_provider": "custom_llm_provider",
|
||||
"input_cost_per_second": 0,
|
||||
"input_cost_per_token": 0,
|
||||
"llm_trace_id": "llm_trace_id",
|
||||
"hanzo_trace_id": "hanzo_trace_id",
|
||||
"max_budget": 0,
|
||||
"max_file_size_mb": 0,
|
||||
"max_retries": 0,
|
||||
@@ -119,7 +115,7 @@ model = client.model.create(
|
||||
model_info={"id": "id"},
|
||||
model_name="model_name",
|
||||
)
|
||||
print(model.llm_params)
|
||||
print(model.hanzo_params)
|
||||
```
|
||||
|
||||
## File uploads
|
||||
@@ -269,9 +265,9 @@ client = response.parse() # get the object that `get_home()` would have returne
|
||||
print(client)
|
||||
```
|
||||
|
||||
These methods return an [`APIResponse`](https://github.com/hanzoai/python-sdk/tree/main/src/hanzoai/_response.py) object.
|
||||
These methods return an [`APIResponse`](https://github.com/hanzoai/python-sdk/tree/main/pkg/hanzoai/_response.py) object.
|
||||
|
||||
The async client returns an [`AsyncAPIResponse`](https://github.com/hanzoai/python-sdk/tree/main/src/hanzoai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.
|
||||
The async client returns an [`AsyncAPIResponse`](https://github.com/hanzoai/python-sdk/tree/main/pkg/hanzoai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.
|
||||
|
||||
#### `.with_streaming_response`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from hanzoai.types import GetHomeResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /">client.<a href="./src/hanzoai/_client.py">get_home</a>() -> <a href="./src/hanzoai/types/get_home_response.py">object</a></code>
|
||||
- <code title="get /">client.<a href="./pkg/hanzoai/_client.py">get_home</a>() -> <a href="./pkg/hanzoai/types/get_home_response.py">object</a></code>
|
||||
|
||||
# Models
|
||||
|
||||
@@ -20,7 +20,7 @@ from hanzoai.types import ModelListResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /v1/models">client.models.<a href="./src/hanzoai/resources/models.py">list</a>(\*\*<a href="src/hanzoai/types/model_list_params.py">params</a>) -> <a href="./src/hanzoai/types/model_list_response.py">object</a></code>
|
||||
- <code title="get /v1/models">client.models.<a href="./pkg/hanzoai/resources/models.py">list</a>(\*\*<a href="pkg/hanzoai/types/model_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model_list_response.py">object</a></code>
|
||||
|
||||
# OpenAI
|
||||
|
||||
@@ -38,11 +38,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /openai/{endpoint}">client.openai.<a href="./src/hanzoai/resources/openai/openai.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/openai_create_response.py">object</a></code>
|
||||
- <code title="get /openai/{endpoint}">client.openai.<a href="./src/hanzoai/resources/openai/openai.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/openai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /openai/{endpoint}">client.openai.<a href="./src/hanzoai/resources/openai/openai.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/openai_update_response.py">object</a></code>
|
||||
- <code title="delete /openai/{endpoint}">client.openai.<a href="./src/hanzoai/resources/openai/openai.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/openai_delete_response.py">object</a></code>
|
||||
- <code title="patch /openai/{endpoint}">client.openai.<a href="./src/hanzoai/resources/openai/openai.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/openai_patch_response.py">object</a></code>
|
||||
- <code title="post /openai/{endpoint}">client.openai.<a href="./pkg/hanzoai/resources/openai/openai.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/openai_create_response.py">object</a></code>
|
||||
- <code title="get /openai/{endpoint}">client.openai.<a href="./pkg/hanzoai/resources/openai/openai.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/openai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /openai/{endpoint}">client.openai.<a href="./pkg/hanzoai/resources/openai/openai.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/openai_update_response.py">object</a></code>
|
||||
- <code title="delete /openai/{endpoint}">client.openai.<a href="./pkg/hanzoai/resources/openai/openai.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/openai_delete_response.py">object</a></code>
|
||||
- <code title="patch /openai/{endpoint}">client.openai.<a href="./pkg/hanzoai/resources/openai/openai.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/openai_patch_response.py">object</a></code>
|
||||
|
||||
## Deployments
|
||||
|
||||
@@ -54,8 +54,8 @@ from hanzoai.types.openai import DeploymentCompleteResponse, DeploymentEmbedResp
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /openai/deployments/{model}/completions">client.openai.deployments.<a href="./src/hanzoai/resources/openai/deployments/deployments.py">complete</a>(model) -> <a href="./src/hanzoai/types/openai/deployment_complete_response.py">object</a></code>
|
||||
- <code title="post /openai/deployments/{model}/embeddings">client.openai.deployments.<a href="./src/hanzoai/resources/openai/deployments/deployments.py">embed</a>(model) -> <a href="./src/hanzoai/types/openai/deployment_embed_response.py">object</a></code>
|
||||
- <code title="post /openai/deployments/{model}/completions">client.openai.deployments.<a href="./pkg/hanzoai/resources/openai/deployments/deployments.py">complete</a>(model) -> <a href="./pkg/hanzoai/types/openai/deployment_complete_response.py">object</a></code>
|
||||
- <code title="post /openai/deployments/{model}/embeddings">client.openai.deployments.<a href="./pkg/hanzoai/resources/openai/deployments/deployments.py">embed</a>(model) -> <a href="./pkg/hanzoai/types/openai/deployment_embed_response.py">object</a></code>
|
||||
|
||||
### Chat
|
||||
|
||||
@@ -67,7 +67,7 @@ from hanzoai.types.openai.deployments import ChatCompleteResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /openai/deployments/{model}/chat/completions">client.openai.deployments.chat.<a href="./src/hanzoai/resources/openai/deployments/chat.py">complete</a>(model) -> <a href="./src/hanzoai/types/openai/deployments/chat_complete_response.py">object</a></code>
|
||||
- <code title="post /openai/deployments/{model}/chat/completions">client.openai.deployments.chat.<a href="./pkg/hanzoai/resources/openai/deployments/chat.py">complete</a>(model) -> <a href="./pkg/hanzoai/types/openai/deployments/chat_complete_response.py">object</a></code>
|
||||
|
||||
# Engines
|
||||
|
||||
@@ -79,8 +79,8 @@ from hanzoai.types import EngineCompleteResponse, EngineEmbedResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /engines/{model}/completions">client.engines.<a href="./src/hanzoai/resources/engines/engines.py">complete</a>(model) -> <a href="./src/hanzoai/types/engine_complete_response.py">object</a></code>
|
||||
- <code title="post /engines/{model}/embeddings">client.engines.<a href="./src/hanzoai/resources/engines/engines.py">embed</a>(model) -> <a href="./src/hanzoai/types/engine_embed_response.py">object</a></code>
|
||||
- <code title="post /engines/{model}/completions">client.engines.<a href="./pkg/hanzoai/resources/engines/engines.py">complete</a>(model) -> <a href="./pkg/hanzoai/types/engine_complete_response.py">object</a></code>
|
||||
- <code title="post /engines/{model}/embeddings">client.engines.<a href="./pkg/hanzoai/resources/engines/engines.py">embed</a>(model) -> <a href="./pkg/hanzoai/types/engine_embed_response.py">object</a></code>
|
||||
|
||||
## Chat
|
||||
|
||||
@@ -92,7 +92,7 @@ from hanzoai.types.engines import ChatCompleteResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /engines/{model}/chat/completions">client.engines.chat.<a href="./src/hanzoai/resources/engines/chat.py">complete</a>(model) -> <a href="./src/hanzoai/types/engines/chat_complete_response.py">object</a></code>
|
||||
- <code title="post /engines/{model}/chat/completions">client.engines.chat.<a href="./pkg/hanzoai/resources/engines/chat.py">complete</a>(model) -> <a href="./pkg/hanzoai/types/engines/chat_complete_response.py">object</a></code>
|
||||
|
||||
# Chat
|
||||
|
||||
@@ -106,7 +106,7 @@ from hanzoai.types.chat import CompletionCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/chat/completions">client.chat.completions.<a href="./src/hanzoai/resources/chat/completions.py">create</a>(\*\*<a href="src/hanzoai/types/chat/completion_create_params.py">params</a>) -> <a href="./src/hanzoai/types/chat/completion_create_response.py">object</a></code>
|
||||
- <code title="post /v1/chat/completions">client.chat.completions.<a href="./pkg/hanzoai/resources/chat/completions.py">create</a>(\*\*<a href="pkg/hanzoai/types/chat/completion_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/chat/completion_create_response.py">object</a></code>
|
||||
|
||||
# Completions
|
||||
|
||||
@@ -118,7 +118,7 @@ from hanzoai.types import CompletionCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /completions">client.completions.<a href="./src/hanzoai/resources/completions.py">create</a>(\*\*<a href="src/hanzoai/types/completion_create_params.py">params</a>) -> <a href="./src/hanzoai/types/completion_create_response.py">object</a></code>
|
||||
- <code title="post /completions">client.completions.<a href="./pkg/hanzoai/resources/completions.py">create</a>(\*\*<a href="pkg/hanzoai/types/completion_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/completion_create_response.py">object</a></code>
|
||||
|
||||
# Embeddings
|
||||
|
||||
@@ -130,7 +130,7 @@ from hanzoai.types import EmbeddingCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /embeddings">client.embeddings.<a href="./src/hanzoai/resources/embeddings.py">create</a>(\*\*<a href="src/hanzoai/types/embedding_create_params.py">params</a>) -> <a href="./src/hanzoai/types/embedding_create_response.py">object</a></code>
|
||||
- <code title="post /embeddings">client.embeddings.<a href="./pkg/hanzoai/resources/embeddings.py">create</a>(\*\*<a href="pkg/hanzoai/types/embedding_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/embedding_create_response.py">object</a></code>
|
||||
|
||||
# Images
|
||||
|
||||
@@ -144,7 +144,7 @@ from hanzoai.types.images import GenerationCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/images/generations">client.images.generations.<a href="./src/hanzoai/resources/images/generations.py">create</a>() -> <a href="./src/hanzoai/types/images/generation_create_response.py">object</a></code>
|
||||
- <code title="post /v1/images/generations">client.images.generations.<a href="./pkg/hanzoai/resources/images/generations.py">create</a>() -> <a href="./pkg/hanzoai/types/images/generation_create_response.py">object</a></code>
|
||||
|
||||
# Audio
|
||||
|
||||
@@ -158,7 +158,7 @@ from hanzoai.types.audio import SpeechCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/audio/speech">client.audio.speech.<a href="./src/hanzoai/resources/audio/speech.py">create</a>() -> <a href="./src/hanzoai/types/audio/speech_create_response.py">object</a></code>
|
||||
- <code title="post /v1/audio/speech">client.audio.speech.<a href="./pkg/hanzoai/resources/audio/speech.py">create</a>() -> <a href="./pkg/hanzoai/types/audio/speech_create_response.py">object</a></code>
|
||||
|
||||
## Transcriptions
|
||||
|
||||
@@ -170,7 +170,7 @@ from hanzoai.types.audio import TranscriptionCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/audio/transcriptions">client.audio.transcriptions.<a href="./src/hanzoai/resources/audio/transcriptions.py">create</a>(\*\*<a href="src/hanzoai/types/audio/transcription_create_params.py">params</a>) -> <a href="./src/hanzoai/types/audio/transcription_create_response.py">object</a></code>
|
||||
- <code title="post /v1/audio/transcriptions">client.audio.transcriptions.<a href="./pkg/hanzoai/resources/audio/transcriptions.py">create</a>(\*\*<a href="pkg/hanzoai/types/audio/transcription_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/audio/transcription_create_response.py">object</a></code>
|
||||
|
||||
# Assistants
|
||||
|
||||
@@ -182,9 +182,9 @@ from hanzoai.types import AssistantCreateResponse, AssistantListResponse, Assist
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/assistants">client.assistants.<a href="./src/hanzoai/resources/assistants.py">create</a>() -> <a href="./src/hanzoai/types/assistant_create_response.py">object</a></code>
|
||||
- <code title="get /v1/assistants">client.assistants.<a href="./src/hanzoai/resources/assistants.py">list</a>() -> <a href="./src/hanzoai/types/assistant_list_response.py">object</a></code>
|
||||
- <code title="delete /v1/assistants/{assistant_id}">client.assistants.<a href="./src/hanzoai/resources/assistants.py">delete</a>(assistant_id) -> <a href="./src/hanzoai/types/assistant_delete_response.py">object</a></code>
|
||||
- <code title="post /v1/assistants">client.assistants.<a href="./pkg/hanzoai/resources/assistants.py">create</a>() -> <a href="./pkg/hanzoai/types/assistant_create_response.py">object</a></code>
|
||||
- <code title="get /v1/assistants">client.assistants.<a href="./pkg/hanzoai/resources/assistants.py">list</a>() -> <a href="./pkg/hanzoai/types/assistant_list_response.py">object</a></code>
|
||||
- <code title="delete /v1/assistants/{assistant_id}">client.assistants.<a href="./pkg/hanzoai/resources/assistants.py">delete</a>(assistant_id) -> <a href="./pkg/hanzoai/types/assistant_delete_response.py">object</a></code>
|
||||
|
||||
# Threads
|
||||
|
||||
@@ -196,8 +196,8 @@ from hanzoai.types import ThreadCreateResponse, ThreadRetrieveResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/threads">client.threads.<a href="./src/hanzoai/resources/threads/threads.py">create</a>() -> <a href="./src/hanzoai/types/thread_create_response.py">object</a></code>
|
||||
- <code title="get /v1/threads/{thread_id}">client.threads.<a href="./src/hanzoai/resources/threads/threads.py">retrieve</a>(thread_id) -> <a href="./src/hanzoai/types/thread_retrieve_response.py">object</a></code>
|
||||
- <code title="post /v1/threads">client.threads.<a href="./pkg/hanzoai/resources/threads/threads.py">create</a>() -> <a href="./pkg/hanzoai/types/thread_create_response.py">object</a></code>
|
||||
- <code title="get /v1/threads/{thread_id}">client.threads.<a href="./pkg/hanzoai/resources/threads/threads.py">retrieve</a>(thread_id) -> <a href="./pkg/hanzoai/types/thread_retrieve_response.py">object</a></code>
|
||||
|
||||
## Messages
|
||||
|
||||
@@ -209,8 +209,8 @@ from hanzoai.types.threads import MessageCreateResponse, MessageListResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/threads/{thread_id}/messages">client.threads.messages.<a href="./src/hanzoai/resources/threads/messages.py">create</a>(thread_id) -> <a href="./src/hanzoai/types/threads/message_create_response.py">object</a></code>
|
||||
- <code title="get /v1/threads/{thread_id}/messages">client.threads.messages.<a href="./src/hanzoai/resources/threads/messages.py">list</a>(thread_id) -> <a href="./src/hanzoai/types/threads/message_list_response.py">object</a></code>
|
||||
- <code title="post /v1/threads/{thread_id}/messages">client.threads.messages.<a href="./pkg/hanzoai/resources/threads/messages.py">create</a>(thread_id) -> <a href="./pkg/hanzoai/types/threads/message_create_response.py">object</a></code>
|
||||
- <code title="get /v1/threads/{thread_id}/messages">client.threads.messages.<a href="./pkg/hanzoai/resources/threads/messages.py">list</a>(thread_id) -> <a href="./pkg/hanzoai/types/threads/message_list_response.py">object</a></code>
|
||||
|
||||
## Runs
|
||||
|
||||
@@ -222,7 +222,7 @@ from hanzoai.types.threads import RunCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/threads/{thread_id}/runs">client.threads.runs.<a href="./src/hanzoai/resources/threads/runs.py">create</a>(thread_id) -> <a href="./src/hanzoai/types/threads/run_create_response.py">object</a></code>
|
||||
- <code title="post /v1/threads/{thread_id}/runs">client.threads.runs.<a href="./pkg/hanzoai/resources/threads/runs.py">create</a>(thread_id) -> <a href="./pkg/hanzoai/types/threads/run_create_response.py">object</a></code>
|
||||
|
||||
# Moderations
|
||||
|
||||
@@ -234,7 +234,7 @@ from hanzoai.types import ModerationCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/moderations">client.moderations.<a href="./src/hanzoai/resources/moderations.py">create</a>() -> <a href="./src/hanzoai/types/moderation_create_response.py">object</a></code>
|
||||
- <code title="post /v1/moderations">client.moderations.<a href="./pkg/hanzoai/resources/moderations.py">create</a>() -> <a href="./pkg/hanzoai/types/moderation_create_response.py">object</a></code>
|
||||
|
||||
# Utils
|
||||
|
||||
@@ -250,9 +250,9 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /utils/supported_openai_params">client.utils.<a href="./src/hanzoai/resources/utils.py">get_supported_openai_params</a>(\*\*<a href="src/hanzoai/types/util_get_supported_openai_params_params.py">params</a>) -> <a href="./src/hanzoai/types/util_get_supported_openai_params_response.py">object</a></code>
|
||||
- <code title="post /utils/token_counter">client.utils.<a href="./src/hanzoai/resources/utils.py">token_counter</a>(\*\*<a href="src/hanzoai/types/util_token_counter_params.py">params</a>) -> <a href="./src/hanzoai/types/util_token_counter_response.py">UtilTokenCounterResponse</a></code>
|
||||
- <code title="post /utils/transform_request">client.utils.<a href="./src/hanzoai/resources/utils.py">transform_request</a>(\*\*<a href="src/hanzoai/types/util_transform_request_params.py">params</a>) -> <a href="./src/hanzoai/types/util_transform_request_response.py">UtilTransformRequestResponse</a></code>
|
||||
- <code title="get /utils/supported_openai_params">client.utils.<a href="./pkg/hanzoai/resources/utils.py">get_supported_openai_params</a>(\*\*<a href="pkg/hanzoai/types/util_get_supported_openai_params_params.py">params</a>) -> <a href="./pkg/hanzoai/types/util_get_supported_openai_params_response.py">object</a></code>
|
||||
- <code title="post /utils/token_counter">client.utils.<a href="./pkg/hanzoai/resources/utils.py">token_counter</a>(\*\*<a href="pkg/hanzoai/types/util_token_counter_params.py">params</a>) -> <a href="./pkg/hanzoai/types/util_token_counter_response.py">UtilTokenCounterResponse</a></code>
|
||||
- <code title="post /utils/transform_request">client.utils.<a href="./pkg/hanzoai/resources/utils.py">transform_request</a>(\*\*<a href="pkg/hanzoai/types/util_transform_request_params.py">params</a>) -> <a href="./pkg/hanzoai/types/util_transform_request_response.py">UtilTransformRequestResponse</a></code>
|
||||
|
||||
# Model
|
||||
|
||||
@@ -269,8 +269,8 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /model/new">client.model.<a href="./src/hanzoai/resources/model/model.py">create</a>(\*\*<a href="src/hanzoai/types/model_create_params.py">params</a>) -> <a href="./src/hanzoai/types/model_create_response.py">object</a></code>
|
||||
- <code title="post /model/delete">client.model.<a href="./src/hanzoai/resources/model/model.py">delete</a>(\*\*<a href="src/hanzoai/types/model_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/model_delete_response.py">object</a></code>
|
||||
- <code title="post /model/new">client.model.<a href="./pkg/hanzoai/resources/model/model.py">create</a>(\*\*<a href="pkg/hanzoai/types/model_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model_create_response.py">object</a></code>
|
||||
- <code title="post /model/delete">client.model.<a href="./pkg/hanzoai/resources/model/model.py">delete</a>(\*\*<a href="pkg/hanzoai/types/model_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model_delete_response.py">object</a></code>
|
||||
|
||||
## Info
|
||||
|
||||
@@ -282,7 +282,7 @@ from hanzoai.types.model import InfoListResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /model/info">client.model.info.<a href="./src/hanzoai/resources/model/info.py">list</a>(\*\*<a href="src/hanzoai/types/model/info_list_params.py">params</a>) -> <a href="./src/hanzoai/types/model/info_list_response.py">object</a></code>
|
||||
- <code title="get /model/info">client.model.info.<a href="./pkg/hanzoai/resources/model/info.py">list</a>(\*\*<a href="pkg/hanzoai/types/model/info_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model/info_list_response.py">object</a></code>
|
||||
|
||||
## Update
|
||||
|
||||
@@ -294,8 +294,8 @@ from hanzoai.types.model import UpdateDeployment, UpdateFullResponse, UpdatePart
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /model/update">client.model.update.<a href="./src/hanzoai/resources/model/update.py">full</a>(\*\*<a href="src/hanzoai/types/model/update_full_params.py">params</a>) -> <a href="./src/hanzoai/types/model/update_full_response.py">object</a></code>
|
||||
- <code title="patch /model/{model_id}/update">client.model.update.<a href="./src/hanzoai/resources/model/update.py">partial</a>(model_id, \*\*<a href="src/hanzoai/types/model/update_partial_params.py">params</a>) -> <a href="./src/hanzoai/types/model/update_partial_response.py">object</a></code>
|
||||
- <code title="post /model/update">client.model.update.<a href="./pkg/hanzoai/resources/model/update.py">full</a>(\*\*<a href="pkg/hanzoai/types/model/update_full_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model/update_full_response.py">object</a></code>
|
||||
- <code title="patch /model/{model_id}/update">client.model.update.<a href="./pkg/hanzoai/resources/model/update.py">partial</a>(model_id, \*\*<a href="pkg/hanzoai/types/model/update_partial_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model/update_partial_response.py">object</a></code>
|
||||
|
||||
# ModelGroup
|
||||
|
||||
@@ -307,7 +307,7 @@ from hanzoai.types import ModelGroupRetrieveInfoResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /model_group/info">client.model_group.<a href="./src/hanzoai/resources/model_group.py">retrieve_info</a>(\*\*<a href="src/hanzoai/types/model_group_retrieve_info_params.py">params</a>) -> <a href="./src/hanzoai/types/model_group_retrieve_info_response.py">object</a></code>
|
||||
- <code title="get /model_group/info">client.model_group.<a href="./pkg/hanzoai/resources/model_group.py">retrieve_info</a>(\*\*<a href="pkg/hanzoai/types/model_group_retrieve_info_params.py">params</a>) -> <a href="./pkg/hanzoai/types/model_group_retrieve_info_response.py">object</a></code>
|
||||
|
||||
# Routes
|
||||
|
||||
@@ -319,7 +319,7 @@ from hanzoai.types import RouteListResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /routes">client.routes.<a href="./src/hanzoai/resources/routes.py">list</a>() -> <a href="./src/hanzoai/types/route_list_response.py">object</a></code>
|
||||
- <code title="get /routes">client.routes.<a href="./pkg/hanzoai/resources/routes.py">list</a>() -> <a href="./pkg/hanzoai/types/route_list_response.py">object</a></code>
|
||||
|
||||
# Responses
|
||||
|
||||
@@ -331,9 +331,9 @@ from hanzoai.types import ResponseCreateResponse, ResponseRetrieveResponse, Resp
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/responses">client.responses.<a href="./src/hanzoai/resources/responses/responses.py">create</a>() -> <a href="./src/hanzoai/types/response_create_response.py">object</a></code>
|
||||
- <code title="get /v1/responses/{response_id}">client.responses.<a href="./src/hanzoai/resources/responses/responses.py">retrieve</a>(response_id) -> <a href="./src/hanzoai/types/response_retrieve_response.py">object</a></code>
|
||||
- <code title="delete /v1/responses/{response_id}">client.responses.<a href="./src/hanzoai/resources/responses/responses.py">delete</a>(response_id) -> <a href="./src/hanzoai/types/response_delete_response.py">object</a></code>
|
||||
- <code title="post /v1/responses">client.responses.<a href="./pkg/hanzoai/resources/responses/responses.py">create</a>() -> <a href="./pkg/hanzoai/types/response_create_response.py">object</a></code>
|
||||
- <code title="get /v1/responses/{response_id}">client.responses.<a href="./pkg/hanzoai/resources/responses/responses.py">retrieve</a>(response_id) -> <a href="./pkg/hanzoai/types/response_retrieve_response.py">object</a></code>
|
||||
- <code title="delete /v1/responses/{response_id}">client.responses.<a href="./pkg/hanzoai/resources/responses/responses.py">delete</a>(response_id) -> <a href="./pkg/hanzoai/types/response_delete_response.py">object</a></code>
|
||||
|
||||
## InputItems
|
||||
|
||||
@@ -345,7 +345,7 @@ from hanzoai.types.responses import InputItemListResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /v1/responses/{response_id}/input_items">client.responses.input_items.<a href="./src/hanzoai/resources/responses/input_items.py">list</a>(response_id) -> <a href="./src/hanzoai/types/responses/input_item_list_response.py">object</a></code>
|
||||
- <code title="get /v1/responses/{response_id}/input_items">client.responses.input_items.<a href="./pkg/hanzoai/resources/responses/input_items.py">list</a>(response_id) -> <a href="./pkg/hanzoai/types/responses/input_item_list_response.py">object</a></code>
|
||||
|
||||
# Batches
|
||||
|
||||
@@ -365,13 +365,13 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/batches">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">create</a>(\*\*<a href="src/hanzoai/types/batch_create_params.py">params</a>) -> <a href="./src/hanzoai/types/batch_create_response.py">object</a></code>
|
||||
- <code title="get /v1/batches/{batch_id}">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">retrieve</a>(batch_id, \*\*<a href="src/hanzoai/types/batch_retrieve_params.py">params</a>) -> <a href="./src/hanzoai/types/batch_retrieve_response.py">object</a></code>
|
||||
- <code title="get /v1/batches">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">list</a>(\*\*<a href="src/hanzoai/types/batch_list_params.py">params</a>) -> <a href="./src/hanzoai/types/batch_list_response.py">object</a></code>
|
||||
- <code title="post /{provider}/v1/batches/{batch_id}/cancel">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">cancel_with_provider</a>(batch_id, \*, provider) -> <a href="./src/hanzoai/types/batch_cancel_with_provider_response.py">object</a></code>
|
||||
- <code title="post /{provider}/v1/batches">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">create_with_provider</a>(provider) -> <a href="./src/hanzoai/types/batch_create_with_provider_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/batches">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">list_with_provider</a>(provider, \*\*<a href="src/hanzoai/types/batch_list_with_provider_params.py">params</a>) -> <a href="./src/hanzoai/types/batch_list_with_provider_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/batches/{batch_id}">client.batches.<a href="./src/hanzoai/resources/batches/batches.py">retrieve_with_provider</a>(batch_id, \*, provider) -> <a href="./src/hanzoai/types/batch_retrieve_with_provider_response.py">object</a></code>
|
||||
- <code title="post /v1/batches">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">create</a>(\*\*<a href="pkg/hanzoai/types/batch_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/batch_create_response.py">object</a></code>
|
||||
- <code title="get /v1/batches/{batch_id}">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">retrieve</a>(batch_id, \*\*<a href="pkg/hanzoai/types/batch_retrieve_params.py">params</a>) -> <a href="./pkg/hanzoai/types/batch_retrieve_response.py">object</a></code>
|
||||
- <code title="get /v1/batches">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">list</a>(\*\*<a href="pkg/hanzoai/types/batch_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/batch_list_response.py">object</a></code>
|
||||
- <code title="post /{provider}/v1/batches/{batch_id}/cancel">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">cancel_with_provider</a>(batch_id, \*, provider) -> <a href="./pkg/hanzoai/types/batch_cancel_with_provider_response.py">object</a></code>
|
||||
- <code title="post /{provider}/v1/batches">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">create_with_provider</a>(provider) -> <a href="./pkg/hanzoai/types/batch_create_with_provider_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/batches">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">list_with_provider</a>(provider, \*\*<a href="pkg/hanzoai/types/batch_list_with_provider_params.py">params</a>) -> <a href="./pkg/hanzoai/types/batch_list_with_provider_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/batches/{batch_id}">client.batches.<a href="./pkg/hanzoai/resources/batches/batches.py">retrieve_with_provider</a>(batch_id, \*, provider) -> <a href="./pkg/hanzoai/types/batch_retrieve_with_provider_response.py">object</a></code>
|
||||
|
||||
## Cancel
|
||||
|
||||
@@ -383,7 +383,7 @@ from hanzoai.types.batches import CancelCancelResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /batches/{batch_id}/cancel">client.batches.cancel.<a href="./src/hanzoai/resources/batches/cancel.py">cancel</a>(batch_id, \*\*<a href="src/hanzoai/types/batches/cancel_cancel_params.py">params</a>) -> <a href="./src/hanzoai/types/batches/cancel_cancel_response.py">object</a></code>
|
||||
- <code title="post /batches/{batch_id}/cancel">client.batches.cancel.<a href="./pkg/hanzoai/resources/batches/cancel.py">cancel</a>(batch_id, \*\*<a href="pkg/hanzoai/types/batches/cancel_cancel_params.py">params</a>) -> <a href="./pkg/hanzoai/types/batches/cancel_cancel_response.py">object</a></code>
|
||||
|
||||
# Rerank
|
||||
|
||||
@@ -395,9 +395,9 @@ from hanzoai.types import RerankCreateResponse, RerankCreateV1Response, RerankCr
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /rerank">client.rerank.<a href="./src/hanzoai/resources/rerank.py">create</a>() -> <a href="./src/hanzoai/types/rerank_create_response.py">object</a></code>
|
||||
- <code title="post /v1/rerank">client.rerank.<a href="./src/hanzoai/resources/rerank.py">create_v1</a>() -> <a href="./src/hanzoai/types/rerank_create_v1_response.py">object</a></code>
|
||||
- <code title="post /v2/rerank">client.rerank.<a href="./src/hanzoai/resources/rerank.py">create_v2</a>() -> <a href="./src/hanzoai/types/rerank_create_v2_response.py">object</a></code>
|
||||
- <code title="post /rerank">client.rerank.<a href="./pkg/hanzoai/resources/rerank.py">create</a>() -> <a href="./pkg/hanzoai/types/rerank_create_response.py">object</a></code>
|
||||
- <code title="post /v1/rerank">client.rerank.<a href="./pkg/hanzoai/resources/rerank.py">create_v1</a>() -> <a href="./pkg/hanzoai/types/rerank_create_v1_response.py">object</a></code>
|
||||
- <code title="post /v2/rerank">client.rerank.<a href="./pkg/hanzoai/resources/rerank.py">create_v2</a>() -> <a href="./pkg/hanzoai/types/rerank_create_v2_response.py">object</a></code>
|
||||
|
||||
# FineTuning
|
||||
|
||||
@@ -406,14 +406,19 @@ Methods:
|
||||
Types:
|
||||
|
||||
```python
|
||||
from hanzoai.types.fine_tuning import JobCreateResponse, JobRetrieveResponse, JobListResponse
|
||||
from hanzoai.types.fine_tuning import (
|
||||
HanzoFineTuningJobCreate,
|
||||
JobCreateResponse,
|
||||
JobRetrieveResponse,
|
||||
JobListResponse,
|
||||
)
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/fine_tuning/jobs">client.fine_tuning.jobs.<a href="./src/hanzoai/resources/fine_tuning/jobs/jobs.py">create</a>(\*\*<a href="src/hanzoai/types/fine_tuning/job_create_params.py">params</a>) -> <a href="./src/hanzoai/types/fine_tuning/job_create_response.py">object</a></code>
|
||||
- <code title="get /v1/fine_tuning/jobs/{fine_tuning_job_id}">client.fine_tuning.jobs.<a href="./src/hanzoai/resources/fine_tuning/jobs/jobs.py">retrieve</a>(fine_tuning_job_id, \*\*<a href="src/hanzoai/types/fine_tuning/job_retrieve_params.py">params</a>) -> <a href="./src/hanzoai/types/fine_tuning/job_retrieve_response.py">object</a></code>
|
||||
- <code title="get /v1/fine_tuning/jobs">client.fine_tuning.jobs.<a href="./src/hanzoai/resources/fine_tuning/jobs/jobs.py">list</a>(\*\*<a href="src/hanzoai/types/fine_tuning/job_list_params.py">params</a>) -> <a href="./src/hanzoai/types/fine_tuning/job_list_response.py">object</a></code>
|
||||
- <code title="post /v1/fine_tuning/jobs">client.fine_tuning.jobs.<a href="./pkg/hanzoai/resources/fine_tuning/jobs/jobs.py">create</a>(\*\*<a href="pkg/hanzoai/types/fine_tuning/job_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/fine_tuning/job_create_response.py">object</a></code>
|
||||
- <code title="get /v1/fine_tuning/jobs/{fine_tuning_job_id}">client.fine_tuning.jobs.<a href="./pkg/hanzoai/resources/fine_tuning/jobs/jobs.py">retrieve</a>(fine_tuning_job_id, \*\*<a href="pkg/hanzoai/types/fine_tuning/job_retrieve_params.py">params</a>) -> <a href="./pkg/hanzoai/types/fine_tuning/job_retrieve_response.py">object</a></code>
|
||||
- <code title="get /v1/fine_tuning/jobs">client.fine_tuning.jobs.<a href="./pkg/hanzoai/resources/fine_tuning/jobs/jobs.py">list</a>(\*\*<a href="pkg/hanzoai/types/fine_tuning/job_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/fine_tuning/job_list_response.py">object</a></code>
|
||||
|
||||
### Cancel
|
||||
|
||||
@@ -425,7 +430,7 @@ from hanzoai.types.fine_tuning.jobs import CancelCreateResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /v1/fine_tuning/jobs/{fine_tuning_job_id}/cancel">client.fine_tuning.jobs.cancel.<a href="./src/hanzoai/resources/fine_tuning/jobs/cancel.py">create</a>(fine_tuning_job_id) -> <a href="./src/hanzoai/types/fine_tuning/jobs/cancel_create_response.py">object</a></code>
|
||||
- <code title="post /v1/fine_tuning/jobs/{fine_tuning_job_id}/cancel">client.fine_tuning.jobs.cancel.<a href="./pkg/hanzoai/resources/fine_tuning/jobs/cancel.py">create</a>(fine_tuning_job_id) -> <a href="./pkg/hanzoai/types/fine_tuning/jobs/cancel_create_response.py">object</a></code>
|
||||
|
||||
# Credentials
|
||||
|
||||
@@ -435,6 +440,8 @@ Types:
|
||||
from hanzoai.types import (
|
||||
CredentialItem,
|
||||
CredentialCreateResponse,
|
||||
CredentialRetrieveResponse,
|
||||
CredentialUpdateResponse,
|
||||
CredentialListResponse,
|
||||
CredentialDeleteResponse,
|
||||
)
|
||||
@@ -442,9 +449,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /credentials">client.credentials.<a href="./src/hanzoai/resources/credentials.py">create</a>(\*\*<a href="src/hanzoai/types/credential_create_params.py">params</a>) -> <a href="./src/hanzoai/types/credential_create_response.py">object</a></code>
|
||||
- <code title="get /credentials">client.credentials.<a href="./src/hanzoai/resources/credentials.py">list</a>() -> <a href="./src/hanzoai/types/credential_list_response.py">object</a></code>
|
||||
- <code title="delete /credentials/{credential_name}">client.credentials.<a href="./src/hanzoai/resources/credentials.py">delete</a>(credential_name) -> <a href="./src/hanzoai/types/credential_delete_response.py">object</a></code>
|
||||
- <code title="post /credentials">client.credentials.<a href="./pkg/hanzoai/resources/credentials.py">create</a>(\*\*<a href="pkg/hanzoai/types/credential_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/credential_create_response.py">object</a></code>
|
||||
- <code title="get /credentials/{credential_name}">client.credentials.<a href="./pkg/hanzoai/resources/credentials.py">retrieve</a>(credential_name) -> <a href="./pkg/hanzoai/types/credential_retrieve_response.py">object</a></code>
|
||||
- <code title="put /credentials/{credential_name}">client.credentials.<a href="./pkg/hanzoai/resources/credentials.py">update</a>(path_credential_name, \*\*<a href="pkg/hanzoai/types/credential_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/credential_update_response.py">object</a></code>
|
||||
- <code title="get /credentials">client.credentials.<a href="./pkg/hanzoai/resources/credentials.py">list</a>() -> <a href="./pkg/hanzoai/types/credential_list_response.py">object</a></code>
|
||||
- <code title="delete /credentials/{credential_name}">client.credentials.<a href="./pkg/hanzoai/resources/credentials.py">delete</a>(credential_name) -> <a href="./pkg/hanzoai/types/credential_delete_response.py">object</a></code>
|
||||
|
||||
# VertexAI
|
||||
|
||||
@@ -462,11 +471,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /vertex_ai/{endpoint}">client.vertex_ai.<a href="./src/hanzoai/resources/vertex_ai.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/vertex_ai_create_response.py">object</a></code>
|
||||
- <code title="get /vertex_ai/{endpoint}">client.vertex_ai.<a href="./src/hanzoai/resources/vertex_ai.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/vertex_ai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /vertex_ai/{endpoint}">client.vertex_ai.<a href="./src/hanzoai/resources/vertex_ai.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/vertex_ai_update_response.py">object</a></code>
|
||||
- <code title="delete /vertex_ai/{endpoint}">client.vertex_ai.<a href="./src/hanzoai/resources/vertex_ai.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/vertex_ai_delete_response.py">object</a></code>
|
||||
- <code title="patch /vertex_ai/{endpoint}">client.vertex_ai.<a href="./src/hanzoai/resources/vertex_ai.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/vertex_ai_patch_response.py">object</a></code>
|
||||
- <code title="post /vertex_ai/{endpoint}">client.vertex_ai.<a href="./pkg/hanzoai/resources/vertex_ai.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/vertex_ai_create_response.py">object</a></code>
|
||||
- <code title="get /vertex_ai/{endpoint}">client.vertex_ai.<a href="./pkg/hanzoai/resources/vertex_ai.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/vertex_ai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /vertex_ai/{endpoint}">client.vertex_ai.<a href="./pkg/hanzoai/resources/vertex_ai.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/vertex_ai_update_response.py">object</a></code>
|
||||
- <code title="delete /vertex_ai/{endpoint}">client.vertex_ai.<a href="./pkg/hanzoai/resources/vertex_ai.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/vertex_ai_delete_response.py">object</a></code>
|
||||
- <code title="patch /vertex_ai/{endpoint}">client.vertex_ai.<a href="./pkg/hanzoai/resources/vertex_ai.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/vertex_ai_patch_response.py">object</a></code>
|
||||
|
||||
# Gemini
|
||||
|
||||
@@ -484,11 +493,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /gemini/{endpoint}">client.gemini.<a href="./src/hanzoai/resources/gemini.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/gemini_create_response.py">object</a></code>
|
||||
- <code title="get /gemini/{endpoint}">client.gemini.<a href="./src/hanzoai/resources/gemini.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/gemini_retrieve_response.py">object</a></code>
|
||||
- <code title="put /gemini/{endpoint}">client.gemini.<a href="./src/hanzoai/resources/gemini.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/gemini_update_response.py">object</a></code>
|
||||
- <code title="delete /gemini/{endpoint}">client.gemini.<a href="./src/hanzoai/resources/gemini.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/gemini_delete_response.py">object</a></code>
|
||||
- <code title="patch /gemini/{endpoint}">client.gemini.<a href="./src/hanzoai/resources/gemini.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/gemini_patch_response.py">object</a></code>
|
||||
- <code title="post /gemini/{endpoint}">client.gemini.<a href="./pkg/hanzoai/resources/gemini.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/gemini_create_response.py">object</a></code>
|
||||
- <code title="get /gemini/{endpoint}">client.gemini.<a href="./pkg/hanzoai/resources/gemini.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/gemini_retrieve_response.py">object</a></code>
|
||||
- <code title="put /gemini/{endpoint}">client.gemini.<a href="./pkg/hanzoai/resources/gemini.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/gemini_update_response.py">object</a></code>
|
||||
- <code title="delete /gemini/{endpoint}">client.gemini.<a href="./pkg/hanzoai/resources/gemini.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/gemini_delete_response.py">object</a></code>
|
||||
- <code title="patch /gemini/{endpoint}">client.gemini.<a href="./pkg/hanzoai/resources/gemini.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/gemini_patch_response.py">object</a></code>
|
||||
|
||||
# Cohere
|
||||
|
||||
@@ -506,11 +515,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /cohere/{endpoint}">client.cohere.<a href="./src/hanzoai/resources/cohere.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/cohere_create_response.py">object</a></code>
|
||||
- <code title="get /cohere/{endpoint}">client.cohere.<a href="./src/hanzoai/resources/cohere.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/cohere_retrieve_response.py">object</a></code>
|
||||
- <code title="put /cohere/{endpoint}">client.cohere.<a href="./src/hanzoai/resources/cohere.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/cohere_update_response.py">object</a></code>
|
||||
- <code title="delete /cohere/{endpoint}">client.cohere.<a href="./src/hanzoai/resources/cohere.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/cohere_delete_response.py">object</a></code>
|
||||
- <code title="patch /cohere/{endpoint}">client.cohere.<a href="./src/hanzoai/resources/cohere.py">modify</a>(endpoint) -> <a href="./src/hanzoai/types/cohere_modify_response.py">object</a></code>
|
||||
- <code title="post /cohere/{endpoint}">client.cohere.<a href="./pkg/hanzoai/resources/cohere.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/cohere_create_response.py">object</a></code>
|
||||
- <code title="get /cohere/{endpoint}">client.cohere.<a href="./pkg/hanzoai/resources/cohere.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/cohere_retrieve_response.py">object</a></code>
|
||||
- <code title="put /cohere/{endpoint}">client.cohere.<a href="./pkg/hanzoai/resources/cohere.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/cohere_update_response.py">object</a></code>
|
||||
- <code title="delete /cohere/{endpoint}">client.cohere.<a href="./pkg/hanzoai/resources/cohere.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/cohere_delete_response.py">object</a></code>
|
||||
- <code title="patch /cohere/{endpoint}">client.cohere.<a href="./pkg/hanzoai/resources/cohere.py">modify</a>(endpoint) -> <a href="./pkg/hanzoai/types/cohere_modify_response.py">object</a></code>
|
||||
|
||||
# Anthropic
|
||||
|
||||
@@ -528,11 +537,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /anthropic/{endpoint}">client.anthropic.<a href="./src/hanzoai/resources/anthropic.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/anthropic_create_response.py">object</a></code>
|
||||
- <code title="get /anthropic/{endpoint}">client.anthropic.<a href="./src/hanzoai/resources/anthropic.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/anthropic_retrieve_response.py">object</a></code>
|
||||
- <code title="put /anthropic/{endpoint}">client.anthropic.<a href="./src/hanzoai/resources/anthropic.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/anthropic_update_response.py">object</a></code>
|
||||
- <code title="delete /anthropic/{endpoint}">client.anthropic.<a href="./src/hanzoai/resources/anthropic.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/anthropic_delete_response.py">object</a></code>
|
||||
- <code title="patch /anthropic/{endpoint}">client.anthropic.<a href="./src/hanzoai/resources/anthropic.py">modify</a>(endpoint) -> <a href="./src/hanzoai/types/anthropic_modify_response.py">object</a></code>
|
||||
- <code title="post /anthropic/{endpoint}">client.anthropic.<a href="./pkg/hanzoai/resources/anthropic.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/anthropic_create_response.py">object</a></code>
|
||||
- <code title="get /anthropic/{endpoint}">client.anthropic.<a href="./pkg/hanzoai/resources/anthropic.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/anthropic_retrieve_response.py">object</a></code>
|
||||
- <code title="put /anthropic/{endpoint}">client.anthropic.<a href="./pkg/hanzoai/resources/anthropic.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/anthropic_update_response.py">object</a></code>
|
||||
- <code title="delete /anthropic/{endpoint}">client.anthropic.<a href="./pkg/hanzoai/resources/anthropic.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/anthropic_delete_response.py">object</a></code>
|
||||
- <code title="patch /anthropic/{endpoint}">client.anthropic.<a href="./pkg/hanzoai/resources/anthropic.py">modify</a>(endpoint) -> <a href="./pkg/hanzoai/types/anthropic_modify_response.py">object</a></code>
|
||||
|
||||
# Bedrock
|
||||
|
||||
@@ -550,11 +559,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /bedrock/{endpoint}">client.bedrock.<a href="./src/hanzoai/resources/bedrock.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/bedrock_create_response.py">object</a></code>
|
||||
- <code title="get /bedrock/{endpoint}">client.bedrock.<a href="./src/hanzoai/resources/bedrock.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/bedrock_retrieve_response.py">object</a></code>
|
||||
- <code title="put /bedrock/{endpoint}">client.bedrock.<a href="./src/hanzoai/resources/bedrock.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/bedrock_update_response.py">object</a></code>
|
||||
- <code title="delete /bedrock/{endpoint}">client.bedrock.<a href="./src/hanzoai/resources/bedrock.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/bedrock_delete_response.py">object</a></code>
|
||||
- <code title="patch /bedrock/{endpoint}">client.bedrock.<a href="./src/hanzoai/resources/bedrock.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/bedrock_patch_response.py">object</a></code>
|
||||
- <code title="post /bedrock/{endpoint}">client.bedrock.<a href="./pkg/hanzoai/resources/bedrock.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/bedrock_create_response.py">object</a></code>
|
||||
- <code title="get /bedrock/{endpoint}">client.bedrock.<a href="./pkg/hanzoai/resources/bedrock.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/bedrock_retrieve_response.py">object</a></code>
|
||||
- <code title="put /bedrock/{endpoint}">client.bedrock.<a href="./pkg/hanzoai/resources/bedrock.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/bedrock_update_response.py">object</a></code>
|
||||
- <code title="delete /bedrock/{endpoint}">client.bedrock.<a href="./pkg/hanzoai/resources/bedrock.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/bedrock_delete_response.py">object</a></code>
|
||||
- <code title="patch /bedrock/{endpoint}">client.bedrock.<a href="./pkg/hanzoai/resources/bedrock.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/bedrock_patch_response.py">object</a></code>
|
||||
|
||||
# EuAssemblyai
|
||||
|
||||
@@ -572,11 +581,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./src/hanzoai/resources/eu_assemblyai.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/eu_assemblyai_create_response.py">object</a></code>
|
||||
- <code title="get /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./src/hanzoai/resources/eu_assemblyai.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/eu_assemblyai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./src/hanzoai/resources/eu_assemblyai.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/eu_assemblyai_update_response.py">object</a></code>
|
||||
- <code title="delete /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./src/hanzoai/resources/eu_assemblyai.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/eu_assemblyai_delete_response.py">object</a></code>
|
||||
- <code title="patch /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./src/hanzoai/resources/eu_assemblyai.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/eu_assemblyai_patch_response.py">object</a></code>
|
||||
- <code title="post /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./pkg/hanzoai/resources/eu_assemblyai.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/eu_assemblyai_create_response.py">object</a></code>
|
||||
- <code title="get /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./pkg/hanzoai/resources/eu_assemblyai.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/eu_assemblyai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./pkg/hanzoai/resources/eu_assemblyai.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/eu_assemblyai_update_response.py">object</a></code>
|
||||
- <code title="delete /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./pkg/hanzoai/resources/eu_assemblyai.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/eu_assemblyai_delete_response.py">object</a></code>
|
||||
- <code title="patch /eu.assemblyai/{endpoint}">client.eu_assemblyai.<a href="./pkg/hanzoai/resources/eu_assemblyai.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/eu_assemblyai_patch_response.py">object</a></code>
|
||||
|
||||
# Assemblyai
|
||||
|
||||
@@ -594,11 +603,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /assemblyai/{endpoint}">client.assemblyai.<a href="./src/hanzoai/resources/assemblyai.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/assemblyai_create_response.py">object</a></code>
|
||||
- <code title="get /assemblyai/{endpoint}">client.assemblyai.<a href="./src/hanzoai/resources/assemblyai.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/assemblyai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /assemblyai/{endpoint}">client.assemblyai.<a href="./src/hanzoai/resources/assemblyai.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/assemblyai_update_response.py">object</a></code>
|
||||
- <code title="delete /assemblyai/{endpoint}">client.assemblyai.<a href="./src/hanzoai/resources/assemblyai.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/assemblyai_delete_response.py">object</a></code>
|
||||
- <code title="patch /assemblyai/{endpoint}">client.assemblyai.<a href="./src/hanzoai/resources/assemblyai.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/assemblyai_patch_response.py">object</a></code>
|
||||
- <code title="post /assemblyai/{endpoint}">client.assemblyai.<a href="./pkg/hanzoai/resources/assemblyai.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/assemblyai_create_response.py">object</a></code>
|
||||
- <code title="get /assemblyai/{endpoint}">client.assemblyai.<a href="./pkg/hanzoai/resources/assemblyai.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/assemblyai_retrieve_response.py">object</a></code>
|
||||
- <code title="put /assemblyai/{endpoint}">client.assemblyai.<a href="./pkg/hanzoai/resources/assemblyai.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/assemblyai_update_response.py">object</a></code>
|
||||
- <code title="delete /assemblyai/{endpoint}">client.assemblyai.<a href="./pkg/hanzoai/resources/assemblyai.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/assemblyai_delete_response.py">object</a></code>
|
||||
- <code title="patch /assemblyai/{endpoint}">client.assemblyai.<a href="./pkg/hanzoai/resources/assemblyai.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/assemblyai_patch_response.py">object</a></code>
|
||||
|
||||
# Azure
|
||||
|
||||
@@ -616,11 +625,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /azure/{endpoint}">client.azure.<a href="./src/hanzoai/resources/azure.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/azure_create_response.py">object</a></code>
|
||||
- <code title="put /azure/{endpoint}">client.azure.<a href="./src/hanzoai/resources/azure.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/azure_update_response.py">object</a></code>
|
||||
- <code title="delete /azure/{endpoint}">client.azure.<a href="./src/hanzoai/resources/azure.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/azure_delete_response.py">object</a></code>
|
||||
- <code title="get /azure/{endpoint}">client.azure.<a href="./src/hanzoai/resources/azure.py">call</a>(endpoint) -> <a href="./src/hanzoai/types/azure_call_response.py">object</a></code>
|
||||
- <code title="patch /azure/{endpoint}">client.azure.<a href="./src/hanzoai/resources/azure.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/azure_patch_response.py">object</a></code>
|
||||
- <code title="post /azure/{endpoint}">client.azure.<a href="./pkg/hanzoai/resources/azure.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/azure_create_response.py">object</a></code>
|
||||
- <code title="put /azure/{endpoint}">client.azure.<a href="./pkg/hanzoai/resources/azure.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/azure_update_response.py">object</a></code>
|
||||
- <code title="delete /azure/{endpoint}">client.azure.<a href="./pkg/hanzoai/resources/azure.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/azure_delete_response.py">object</a></code>
|
||||
- <code title="get /azure/{endpoint}">client.azure.<a href="./pkg/hanzoai/resources/azure.py">call</a>(endpoint) -> <a href="./pkg/hanzoai/types/azure_call_response.py">object</a></code>
|
||||
- <code title="patch /azure/{endpoint}">client.azure.<a href="./pkg/hanzoai/resources/azure.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/azure_patch_response.py">object</a></code>
|
||||
|
||||
# Langfuse
|
||||
|
||||
@@ -638,11 +647,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /langfuse/{endpoint}">client.langfuse.<a href="./src/hanzoai/resources/langfuse.py">create</a>(endpoint) -> <a href="./src/hanzoai/types/langfuse_create_response.py">object</a></code>
|
||||
- <code title="get /langfuse/{endpoint}">client.langfuse.<a href="./src/hanzoai/resources/langfuse.py">retrieve</a>(endpoint) -> <a href="./src/hanzoai/types/langfuse_retrieve_response.py">object</a></code>
|
||||
- <code title="put /langfuse/{endpoint}">client.langfuse.<a href="./src/hanzoai/resources/langfuse.py">update</a>(endpoint) -> <a href="./src/hanzoai/types/langfuse_update_response.py">object</a></code>
|
||||
- <code title="delete /langfuse/{endpoint}">client.langfuse.<a href="./src/hanzoai/resources/langfuse.py">delete</a>(endpoint) -> <a href="./src/hanzoai/types/langfuse_delete_response.py">object</a></code>
|
||||
- <code title="patch /langfuse/{endpoint}">client.langfuse.<a href="./src/hanzoai/resources/langfuse.py">patch</a>(endpoint) -> <a href="./src/hanzoai/types/langfuse_patch_response.py">object</a></code>
|
||||
- <code title="post /langfuse/{endpoint}">client.langfuse.<a href="./pkg/hanzoai/resources/langfuse.py">create</a>(endpoint) -> <a href="./pkg/hanzoai/types/langfuse_create_response.py">object</a></code>
|
||||
- <code title="get /langfuse/{endpoint}">client.langfuse.<a href="./pkg/hanzoai/resources/langfuse.py">retrieve</a>(endpoint) -> <a href="./pkg/hanzoai/types/langfuse_retrieve_response.py">object</a></code>
|
||||
- <code title="put /langfuse/{endpoint}">client.langfuse.<a href="./pkg/hanzoai/resources/langfuse.py">update</a>(endpoint) -> <a href="./pkg/hanzoai/types/langfuse_update_response.py">object</a></code>
|
||||
- <code title="delete /langfuse/{endpoint}">client.langfuse.<a href="./pkg/hanzoai/resources/langfuse.py">delete</a>(endpoint) -> <a href="./pkg/hanzoai/types/langfuse_delete_response.py">object</a></code>
|
||||
- <code title="patch /langfuse/{endpoint}">client.langfuse.<a href="./pkg/hanzoai/resources/langfuse.py">patch</a>(endpoint) -> <a href="./pkg/hanzoai/types/langfuse_patch_response.py">object</a></code>
|
||||
|
||||
# Config
|
||||
|
||||
@@ -661,10 +670,10 @@ from hanzoai.types.config import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /config/pass_through_endpoint">client.config.pass_through_endpoint.<a href="./src/hanzoai/resources/config/pass_through_endpoint.py">create</a>(\*\*<a href="src/hanzoai/types/config/pass_through_endpoint_create_params.py">params</a>) -> <a href="./src/hanzoai/types/config/pass_through_endpoint_create_response.py">object</a></code>
|
||||
- <code title="post /config/pass_through_endpoint/{endpoint_id}">client.config.pass_through_endpoint.<a href="./src/hanzoai/resources/config/pass_through_endpoint.py">update</a>(endpoint_id) -> <a href="./src/hanzoai/types/config/pass_through_endpoint_update_response.py">object</a></code>
|
||||
- <code title="get /config/pass_through_endpoint">client.config.pass_through_endpoint.<a href="./src/hanzoai/resources/config/pass_through_endpoint.py">list</a>(\*\*<a href="src/hanzoai/types/config/pass_through_endpoint_list_params.py">params</a>) -> <a href="./src/hanzoai/types/config/pass_through_endpoint_response.py">PassThroughEndpointResponse</a></code>
|
||||
- <code title="delete /config/pass_through_endpoint">client.config.pass_through_endpoint.<a href="./src/hanzoai/resources/config/pass_through_endpoint.py">delete</a>(\*\*<a href="src/hanzoai/types/config/pass_through_endpoint_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/config/pass_through_endpoint_response.py">PassThroughEndpointResponse</a></code>
|
||||
- <code title="post /config/pass_through_endpoint">client.config.pass_through_endpoint.<a href="./pkg/hanzoai/resources/config/pass_through_endpoint.py">create</a>(\*\*<a href="pkg/hanzoai/types/config/pass_through_endpoint_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/config/pass_through_endpoint_create_response.py">object</a></code>
|
||||
- <code title="post /config/pass_through_endpoint/{endpoint_id}">client.config.pass_through_endpoint.<a href="./pkg/hanzoai/resources/config/pass_through_endpoint.py">update</a>(endpoint_id) -> <a href="./pkg/hanzoai/types/config/pass_through_endpoint_update_response.py">object</a></code>
|
||||
- <code title="get /config/pass_through_endpoint">client.config.pass_through_endpoint.<a href="./pkg/hanzoai/resources/config/pass_through_endpoint.py">list</a>(\*\*<a href="pkg/hanzoai/types/config/pass_through_endpoint_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/config/pass_through_endpoint_response.py">PassThroughEndpointResponse</a></code>
|
||||
- <code title="delete /config/pass_through_endpoint">client.config.pass_through_endpoint.<a href="./pkg/hanzoai/resources/config/pass_through_endpoint.py">delete</a>(\*\*<a href="pkg/hanzoai/types/config/pass_through_endpoint_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/config/pass_through_endpoint_response.py">PassThroughEndpointResponse</a></code>
|
||||
|
||||
# Test
|
||||
|
||||
@@ -676,7 +685,7 @@ from hanzoai.types import TestPingResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /test">client.test.<a href="./src/hanzoai/resources/test.py">ping</a>() -> <a href="./src/hanzoai/types/test_ping_response.py">object</a></code>
|
||||
- <code title="get /test">client.test.<a href="./pkg/hanzoai/resources/test.py">ping</a>() -> <a href="./pkg/hanzoai/types/test_ping_response.py">object</a></code>
|
||||
|
||||
# Health
|
||||
|
||||
@@ -694,11 +703,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /health">client.health.<a href="./src/hanzoai/resources/health.py">check_all</a>(\*\*<a href="src/hanzoai/types/health_check_all_params.py">params</a>) -> <a href="./src/hanzoai/types/health_check_all_response.py">object</a></code>
|
||||
- <code title="get /health/liveliness">client.health.<a href="./src/hanzoai/resources/health.py">check_liveliness</a>() -> <a href="./src/hanzoai/types/health_check_liveliness_response.py">object</a></code>
|
||||
- <code title="get /health/liveness">client.health.<a href="./src/hanzoai/resources/health.py">check_liveness</a>() -> <a href="./src/hanzoai/types/health_check_liveness_response.py">object</a></code>
|
||||
- <code title="get /health/readiness">client.health.<a href="./src/hanzoai/resources/health.py">check_readiness</a>() -> <a href="./src/hanzoai/types/health_check_readiness_response.py">object</a></code>
|
||||
- <code title="get /health/services">client.health.<a href="./src/hanzoai/resources/health.py">check_services</a>(\*\*<a href="src/hanzoai/types/health_check_services_params.py">params</a>) -> <a href="./src/hanzoai/types/health_check_services_response.py">object</a></code>
|
||||
- <code title="get /health">client.health.<a href="./pkg/hanzoai/resources/health.py">check_all</a>(\*\*<a href="pkg/hanzoai/types/health_check_all_params.py">params</a>) -> <a href="./pkg/hanzoai/types/health_check_all_response.py">object</a></code>
|
||||
- <code title="get /health/liveliness">client.health.<a href="./pkg/hanzoai/resources/health.py">check_liveliness</a>() -> <a href="./pkg/hanzoai/types/health_check_liveliness_response.py">object</a></code>
|
||||
- <code title="get /health/liveness">client.health.<a href="./pkg/hanzoai/resources/health.py">check_liveness</a>() -> <a href="./pkg/hanzoai/types/health_check_liveness_response.py">object</a></code>
|
||||
- <code title="get /health/readiness">client.health.<a href="./pkg/hanzoai/resources/health.py">check_readiness</a>() -> <a href="./pkg/hanzoai/types/health_check_readiness_response.py">object</a></code>
|
||||
- <code title="get /health/services">client.health.<a href="./pkg/hanzoai/resources/health.py">check_services</a>(\*\*<a href="pkg/hanzoai/types/health_check_services_params.py">params</a>) -> <a href="./pkg/hanzoai/types/health_check_services_response.py">object</a></code>
|
||||
|
||||
# Active
|
||||
|
||||
@@ -710,7 +719,7 @@ from hanzoai.types import ActiveListCallbacksResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /active/callbacks">client.active.<a href="./src/hanzoai/resources/active.py">list_callbacks</a>() -> <a href="./src/hanzoai/types/active_list_callbacks_response.py">object</a></code>
|
||||
- <code title="get /active/callbacks">client.active.<a href="./pkg/hanzoai/resources/active.py">list_callbacks</a>() -> <a href="./pkg/hanzoai/types/active_list_callbacks_response.py">object</a></code>
|
||||
|
||||
# Settings
|
||||
|
||||
@@ -722,7 +731,7 @@ from hanzoai.types import SettingRetrieveResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /settings">client.settings.<a href="./src/hanzoai/resources/settings.py">retrieve</a>() -> <a href="./src/hanzoai/types/setting_retrieve_response.py">object</a></code>
|
||||
- <code title="get /settings">client.settings.<a href="./pkg/hanzoai/resources/settings.py">retrieve</a>() -> <a href="./pkg/hanzoai/types/setting_retrieve_response.py">object</a></code>
|
||||
|
||||
# Key
|
||||
|
||||
@@ -744,15 +753,15 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /key/update">client.key.<a href="./src/hanzoai/resources/key/key.py">update</a>(\*\*<a href="src/hanzoai/types/key_update_params.py">params</a>) -> <a href="./src/hanzoai/types/key_update_response.py">object</a></code>
|
||||
- <code title="get /key/list">client.key.<a href="./src/hanzoai/resources/key/key.py">list</a>(\*\*<a href="src/hanzoai/types/key_list_params.py">params</a>) -> <a href="./src/hanzoai/types/key_list_response.py">KeyListResponse</a></code>
|
||||
- <code title="post /key/delete">client.key.<a href="./src/hanzoai/resources/key/key.py">delete</a>(\*\*<a href="src/hanzoai/types/key_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/key_delete_response.py">object</a></code>
|
||||
- <code title="post /key/block">client.key.<a href="./src/hanzoai/resources/key/key.py">block</a>(\*\*<a href="src/hanzoai/types/key_block_params.py">params</a>) -> <a href="./src/hanzoai/types/key_block_response.py">Optional[KeyBlockResponse]</a></code>
|
||||
- <code title="post /key/health">client.key.<a href="./src/hanzoai/resources/key/key.py">check_health</a>() -> <a href="./src/hanzoai/types/key_check_health_response.py">KeyCheckHealthResponse</a></code>
|
||||
- <code title="post /key/generate">client.key.<a href="./src/hanzoai/resources/key/key.py">generate</a>(\*\*<a href="src/hanzoai/types/key_generate_params.py">params</a>) -> <a href="./src/hanzoai/types/generate_key_response.py">GenerateKeyResponse</a></code>
|
||||
- <code title="post /key/{key}/regenerate">client.key.<a href="./src/hanzoai/resources/key/key.py">regenerate_by_key</a>(path_key, \*\*<a href="src/hanzoai/types/key_regenerate_by_key_params.py">params</a>) -> <a href="./src/hanzoai/types/generate_key_response.py">Optional[GenerateKeyResponse]</a></code>
|
||||
- <code title="get /key/info">client.key.<a href="./src/hanzoai/resources/key/key.py">retrieve_info</a>(\*\*<a href="src/hanzoai/types/key_retrieve_info_params.py">params</a>) -> <a href="./src/hanzoai/types/key_retrieve_info_response.py">object</a></code>
|
||||
- <code title="post /key/unblock">client.key.<a href="./src/hanzoai/resources/key/key.py">unblock</a>(\*\*<a href="src/hanzoai/types/key_unblock_params.py">params</a>) -> <a href="./src/hanzoai/types/key_unblock_response.py">object</a></code>
|
||||
- <code title="post /key/update">client.key.<a href="./pkg/hanzoai/resources/key/key.py">update</a>(\*\*<a href="pkg/hanzoai/types/key_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/key_update_response.py">object</a></code>
|
||||
- <code title="get /key/list">client.key.<a href="./pkg/hanzoai/resources/key/key.py">list</a>(\*\*<a href="pkg/hanzoai/types/key_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/key_list_response.py">KeyListResponse</a></code>
|
||||
- <code title="post /key/delete">client.key.<a href="./pkg/hanzoai/resources/key/key.py">delete</a>(\*\*<a href="pkg/hanzoai/types/key_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/key_delete_response.py">object</a></code>
|
||||
- <code title="post /key/block">client.key.<a href="./pkg/hanzoai/resources/key/key.py">block</a>(\*\*<a href="pkg/hanzoai/types/key_block_params.py">params</a>) -> <a href="./pkg/hanzoai/types/key_block_response.py">Optional[KeyBlockResponse]</a></code>
|
||||
- <code title="post /key/health">client.key.<a href="./pkg/hanzoai/resources/key/key.py">check_health</a>() -> <a href="./pkg/hanzoai/types/key_check_health_response.py">KeyCheckHealthResponse</a></code>
|
||||
- <code title="post /key/generate">client.key.<a href="./pkg/hanzoai/resources/key/key.py">generate</a>(\*\*<a href="pkg/hanzoai/types/key_generate_params.py">params</a>) -> <a href="./pkg/hanzoai/types/generate_key_response.py">GenerateKeyResponse</a></code>
|
||||
- <code title="post /key/{key}/regenerate">client.key.<a href="./pkg/hanzoai/resources/key/key.py">regenerate_by_key</a>(path_key, \*\*<a href="pkg/hanzoai/types/key_regenerate_by_key_params.py">params</a>) -> <a href="./pkg/hanzoai/types/generate_key_response.py">Optional[GenerateKeyResponse]</a></code>
|
||||
- <code title="get /key/info">client.key.<a href="./pkg/hanzoai/resources/key/key.py">retrieve_info</a>(\*\*<a href="pkg/hanzoai/types/key_retrieve_info_params.py">params</a>) -> <a href="./pkg/hanzoai/types/key_retrieve_info_response.py">object</a></code>
|
||||
- <code title="post /key/unblock">client.key.<a href="./pkg/hanzoai/resources/key/key.py">unblock</a>(\*\*<a href="pkg/hanzoai/types/key_unblock_params.py">params</a>) -> <a href="./pkg/hanzoai/types/key_unblock_response.py">object</a></code>
|
||||
|
||||
## Regenerate
|
||||
|
||||
@@ -778,11 +787,11 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /user/new">client.user.<a href="./src/hanzoai/resources/user.py">create</a>(\*\*<a href="src/hanzoai/types/user_create_params.py">params</a>) -> <a href="./src/hanzoai/types/user_create_response.py">UserCreateResponse</a></code>
|
||||
- <code title="post /user/update">client.user.<a href="./src/hanzoai/resources/user.py">update</a>(\*\*<a href="src/hanzoai/types/user_update_params.py">params</a>) -> <a href="./src/hanzoai/types/user_update_response.py">object</a></code>
|
||||
- <code title="get /user/get_users">client.user.<a href="./src/hanzoai/resources/user.py">list</a>(\*\*<a href="src/hanzoai/types/user_list_params.py">params</a>) -> <a href="./src/hanzoai/types/user_list_response.py">object</a></code>
|
||||
- <code title="post /user/delete">client.user.<a href="./src/hanzoai/resources/user.py">delete</a>(\*\*<a href="src/hanzoai/types/user_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/user_delete_response.py">object</a></code>
|
||||
- <code title="get /user/info">client.user.<a href="./src/hanzoai/resources/user.py">retrieve_info</a>(\*\*<a href="src/hanzoai/types/user_retrieve_info_params.py">params</a>) -> <a href="./src/hanzoai/types/user_retrieve_info_response.py">object</a></code>
|
||||
- <code title="post /user/new">client.user.<a href="./pkg/hanzoai/resources/user.py">create</a>(\*\*<a href="pkg/hanzoai/types/user_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/user_create_response.py">UserCreateResponse</a></code>
|
||||
- <code title="post /user/update">client.user.<a href="./pkg/hanzoai/resources/user.py">update</a>(\*\*<a href="pkg/hanzoai/types/user_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/user_update_response.py">object</a></code>
|
||||
- <code title="get /user/get_users">client.user.<a href="./pkg/hanzoai/resources/user.py">list</a>(\*\*<a href="pkg/hanzoai/types/user_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/user_list_response.py">object</a></code>
|
||||
- <code title="post /user/delete">client.user.<a href="./pkg/hanzoai/resources/user.py">delete</a>(\*\*<a href="pkg/hanzoai/types/user_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/user_delete_response.py">object</a></code>
|
||||
- <code title="get /user/info">client.user.<a href="./pkg/hanzoai/resources/user.py">retrieve_info</a>(\*\*<a href="pkg/hanzoai/types/user_retrieve_info_params.py">params</a>) -> <a href="./pkg/hanzoai/types/user_retrieve_info_response.py">object</a></code>
|
||||
|
||||
# Team
|
||||
|
||||
@@ -791,8 +800,10 @@ Types:
|
||||
```python
|
||||
from hanzoai.types import (
|
||||
BlockTeamRequest,
|
||||
HanzoModelTable,
|
||||
HanzoTeamTable,
|
||||
HanzoUserTable,
|
||||
Member,
|
||||
TeamCreateResponse,
|
||||
TeamUpdateResponse,
|
||||
TeamListResponse,
|
||||
TeamDeleteResponse,
|
||||
@@ -809,18 +820,18 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /team/new">client.team.<a href="./src/hanzoai/resources/team/team.py">create</a>(\*\*<a href="src/hanzoai/types/team_create_params.py">params</a>) -> <a href="./src/hanzoai/types/team_create_response.py">TeamCreateResponse</a></code>
|
||||
- <code title="post /team/update">client.team.<a href="./src/hanzoai/resources/team/team.py">update</a>(\*\*<a href="src/hanzoai/types/team_update_params.py">params</a>) -> <a href="./src/hanzoai/types/team_update_response.py">object</a></code>
|
||||
- <code title="get /team/list">client.team.<a href="./src/hanzoai/resources/team/team.py">list</a>(\*\*<a href="src/hanzoai/types/team_list_params.py">params</a>) -> <a href="./src/hanzoai/types/team_list_response.py">object</a></code>
|
||||
- <code title="post /team/delete">client.team.<a href="./src/hanzoai/resources/team/team.py">delete</a>(\*\*<a href="src/hanzoai/types/team_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/team_delete_response.py">object</a></code>
|
||||
- <code title="post /team/member_add">client.team.<a href="./src/hanzoai/resources/team/team.py">add_member</a>(\*\*<a href="src/hanzoai/types/team_add_member_params.py">params</a>) -> <a href="./src/hanzoai/types/team_add_member_response.py">TeamAddMemberResponse</a></code>
|
||||
- <code title="post /team/block">client.team.<a href="./src/hanzoai/resources/team/team.py">block</a>(\*\*<a href="src/hanzoai/types/team_block_params.py">params</a>) -> <a href="./src/hanzoai/types/team_block_response.py">object</a></code>
|
||||
- <code title="post /team/{team_id}/disable_logging">client.team.<a href="./src/hanzoai/resources/team/team.py">disable_logging</a>(team_id) -> <a href="./src/hanzoai/types/team_disable_logging_response.py">object</a></code>
|
||||
- <code title="get /team/available">client.team.<a href="./src/hanzoai/resources/team/team.py">list_available</a>(\*\*<a href="src/hanzoai/types/team_list_available_params.py">params</a>) -> <a href="./src/hanzoai/types/team_list_available_response.py">object</a></code>
|
||||
- <code title="post /team/member_delete">client.team.<a href="./src/hanzoai/resources/team/team.py">remove_member</a>(\*\*<a href="src/hanzoai/types/team_remove_member_params.py">params</a>) -> <a href="./src/hanzoai/types/team_remove_member_response.py">object</a></code>
|
||||
- <code title="get /team/info">client.team.<a href="./src/hanzoai/resources/team/team.py">retrieve_info</a>(\*\*<a href="src/hanzoai/types/team_retrieve_info_params.py">params</a>) -> <a href="./src/hanzoai/types/team_retrieve_info_response.py">object</a></code>
|
||||
- <code title="post /team/unblock">client.team.<a href="./src/hanzoai/resources/team/team.py">unblock</a>(\*\*<a href="src/hanzoai/types/team_unblock_params.py">params</a>) -> <a href="./src/hanzoai/types/team_unblock_response.py">object</a></code>
|
||||
- <code title="post /team/member_update">client.team.<a href="./src/hanzoai/resources/team/team.py">update_member</a>(\*\*<a href="src/hanzoai/types/team_update_member_params.py">params</a>) -> <a href="./src/hanzoai/types/team_update_member_response.py">TeamUpdateMemberResponse</a></code>
|
||||
- <code title="post /team/new">client.team.<a href="./pkg/hanzoai/resources/team/team.py">create</a>(\*\*<a href="pkg/hanzoai/types/team_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/hanzo_team_table.py">HanzoTeamTable</a></code>
|
||||
- <code title="post /team/update">client.team.<a href="./pkg/hanzoai/resources/team/team.py">update</a>(\*\*<a href="pkg/hanzoai/types/team_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_update_response.py">object</a></code>
|
||||
- <code title="get /team/list">client.team.<a href="./pkg/hanzoai/resources/team/team.py">list</a>(\*\*<a href="pkg/hanzoai/types/team_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_list_response.py">object</a></code>
|
||||
- <code title="post /team/delete">client.team.<a href="./pkg/hanzoai/resources/team/team.py">delete</a>(\*\*<a href="pkg/hanzoai/types/team_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_delete_response.py">object</a></code>
|
||||
- <code title="post /team/member_add">client.team.<a href="./pkg/hanzoai/resources/team/team.py">add_member</a>(\*\*<a href="pkg/hanzoai/types/team_add_member_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_add_member_response.py">TeamAddMemberResponse</a></code>
|
||||
- <code title="post /team/block">client.team.<a href="./pkg/hanzoai/resources/team/team.py">block</a>(\*\*<a href="pkg/hanzoai/types/team_block_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_block_response.py">object</a></code>
|
||||
- <code title="post /team/{team_id}/disable_logging">client.team.<a href="./pkg/hanzoai/resources/team/team.py">disable_logging</a>(team_id) -> <a href="./pkg/hanzoai/types/team_disable_logging_response.py">object</a></code>
|
||||
- <code title="get /team/available">client.team.<a href="./pkg/hanzoai/resources/team/team.py">list_available</a>(\*\*<a href="pkg/hanzoai/types/team_list_available_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_list_available_response.py">object</a></code>
|
||||
- <code title="post /team/member_delete">client.team.<a href="./pkg/hanzoai/resources/team/team.py">remove_member</a>(\*\*<a href="pkg/hanzoai/types/team_remove_member_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_remove_member_response.py">object</a></code>
|
||||
- <code title="get /team/info">client.team.<a href="./pkg/hanzoai/resources/team/team.py">retrieve_info</a>(\*\*<a href="pkg/hanzoai/types/team_retrieve_info_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_retrieve_info_response.py">object</a></code>
|
||||
- <code title="post /team/unblock">client.team.<a href="./pkg/hanzoai/resources/team/team.py">unblock</a>(\*\*<a href="pkg/hanzoai/types/team_unblock_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_unblock_response.py">object</a></code>
|
||||
- <code title="post /team/member_update">client.team.<a href="./pkg/hanzoai/resources/team/team.py">update_member</a>(\*\*<a href="pkg/hanzoai/types/team_update_member_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team_update_member_response.py">TeamUpdateMemberResponse</a></code>
|
||||
|
||||
## Model
|
||||
|
||||
@@ -832,8 +843,8 @@ from hanzoai.types.team import ModelAddResponse, ModelRemoveResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /team/model/add">client.team.model.<a href="./src/hanzoai/resources/team/model.py">add</a>(\*\*<a href="src/hanzoai/types/team/model_add_params.py">params</a>) -> <a href="./src/hanzoai/types/team/model_add_response.py">object</a></code>
|
||||
- <code title="post /team/model/delete">client.team.model.<a href="./src/hanzoai/resources/team/model.py">remove</a>(\*\*<a href="src/hanzoai/types/team/model_remove_params.py">params</a>) -> <a href="./src/hanzoai/types/team/model_remove_response.py">object</a></code>
|
||||
- <code title="post /team/model/add">client.team.model.<a href="./pkg/hanzoai/resources/team/model.py">add</a>(\*\*<a href="pkg/hanzoai/types/team/model_add_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team/model_add_response.py">object</a></code>
|
||||
- <code title="post /team/model/delete">client.team.model.<a href="./pkg/hanzoai/resources/team/model.py">remove</a>(\*\*<a href="pkg/hanzoai/types/team/model_remove_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team/model_remove_response.py">object</a></code>
|
||||
|
||||
## Callback
|
||||
|
||||
@@ -845,8 +856,8 @@ from hanzoai.types.team import CallbackRetrieveResponse, CallbackAddResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /team/{team_id}/callback">client.team.callback.<a href="./src/hanzoai/resources/team/callback.py">retrieve</a>(team_id) -> <a href="./src/hanzoai/types/team/callback_retrieve_response.py">object</a></code>
|
||||
- <code title="post /team/{team_id}/callback">client.team.callback.<a href="./src/hanzoai/resources/team/callback.py">add</a>(team_id, \*\*<a href="src/hanzoai/types/team/callback_add_params.py">params</a>) -> <a href="./src/hanzoai/types/team/callback_add_response.py">object</a></code>
|
||||
- <code title="get /team/{team_id}/callback">client.team.callback.<a href="./pkg/hanzoai/resources/team/callback.py">retrieve</a>(team_id) -> <a href="./pkg/hanzoai/types/team/callback_retrieve_response.py">object</a></code>
|
||||
- <code title="post /team/{team_id}/callback">client.team.callback.<a href="./pkg/hanzoai/resources/team/callback.py">add</a>(team_id, \*\*<a href="pkg/hanzoai/types/team/callback_add_params.py">params</a>) -> <a href="./pkg/hanzoai/types/team/callback_add_response.py">object</a></code>
|
||||
|
||||
# Organization
|
||||
|
||||
@@ -854,39 +865,41 @@ Types:
|
||||
|
||||
```python
|
||||
from hanzoai.types import (
|
||||
BudgetTable,
|
||||
OrgMember,
|
||||
OrganizationMembershipTable,
|
||||
OrganizationTableWithMembers,
|
||||
UserRoles,
|
||||
OrganizationCreateResponse,
|
||||
OrganizationUpdateResponse,
|
||||
OrganizationListResponse,
|
||||
OrganizationDeleteResponse,
|
||||
OrganizationAddMemberResponse,
|
||||
OrganizationDeleteMemberResponse,
|
||||
OrganizationUpdateMemberResponse,
|
||||
)
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /organization/new">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">create</a>(\*\*<a href="src/hanzoai/types/organization_create_params.py">params</a>) -> <a href="./src/hanzoai/types/organization_create_response.py">OrganizationCreateResponse</a></code>
|
||||
- <code title="patch /organization/update">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">update</a>(\*\*<a href="src/hanzoai/types/organization_update_params.py">params</a>) -> <a href="./src/hanzoai/types/organization_update_response.py">OrganizationUpdateResponse</a></code>
|
||||
- <code title="get /organization/list">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">list</a>() -> <a href="./src/hanzoai/types/organization_list_response.py">OrganizationListResponse</a></code>
|
||||
- <code title="delete /organization/delete">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">delete</a>(\*\*<a href="src/hanzoai/types/organization_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/organization_delete_response.py">OrganizationDeleteResponse</a></code>
|
||||
- <code title="post /organization/member_add">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">add_member</a>(\*\*<a href="src/hanzoai/types/organization_add_member_params.py">params</a>) -> <a href="./src/hanzoai/types/organization_add_member_response.py">OrganizationAddMemberResponse</a></code>
|
||||
- <code title="delete /organization/member_delete">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">delete_member</a>(\*\*<a href="src/hanzoai/types/organization_delete_member_params.py">params</a>) -> <a href="./src/hanzoai/types/organization_delete_member_response.py">object</a></code>
|
||||
- <code title="patch /organization/member_update">client.organization.<a href="./src/hanzoai/resources/organization/organization.py">update_member</a>(\*\*<a href="src/hanzoai/types/organization_update_member_params.py">params</a>) -> <a href="./src/hanzoai/types/organization_update_member_response.py">OrganizationUpdateMemberResponse</a></code>
|
||||
- <code title="post /organization/new">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">create</a>(\*\*<a href="pkg/hanzoai/types/organization_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_create_response.py">OrganizationCreateResponse</a></code>
|
||||
- <code title="patch /organization/update">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">update</a>(\*\*<a href="pkg/hanzoai/types/organization_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_table_with_members.py">OrganizationTableWithMembers</a></code>
|
||||
- <code title="get /organization/list">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">list</a>() -> <a href="./pkg/hanzoai/types/organization_list_response.py">OrganizationListResponse</a></code>
|
||||
- <code title="delete /organization/delete">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">delete</a>(\*\*<a href="pkg/hanzoai/types/organization_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_delete_response.py">OrganizationDeleteResponse</a></code>
|
||||
- <code title="post /organization/member_add">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">add_member</a>(\*\*<a href="pkg/hanzoai/types/organization_add_member_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_add_member_response.py">OrganizationAddMemberResponse</a></code>
|
||||
- <code title="delete /organization/member_delete">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">delete_member</a>(\*\*<a href="pkg/hanzoai/types/organization_delete_member_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_delete_member_response.py">object</a></code>
|
||||
- <code title="patch /organization/member_update">client.organization.<a href="./pkg/hanzoai/resources/organization/organization.py">update_member</a>(\*\*<a href="pkg/hanzoai/types/organization_update_member_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_membership_table.py">OrganizationMembershipTable</a></code>
|
||||
|
||||
## Info
|
||||
|
||||
Types:
|
||||
|
||||
```python
|
||||
from hanzoai.types.organization import InfoRetrieveResponse, InfoDeprecatedResponse
|
||||
from hanzoai.types.organization import InfoDeprecatedResponse
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /organization/info">client.organization.info.<a href="./src/hanzoai/resources/organization/info.py">retrieve</a>(\*\*<a href="src/hanzoai/types/organization/info_retrieve_params.py">params</a>) -> <a href="./src/hanzoai/types/organization/info_retrieve_response.py">InfoRetrieveResponse</a></code>
|
||||
- <code title="post /organization/info">client.organization.info.<a href="./src/hanzoai/resources/organization/info.py">deprecated</a>(\*\*<a href="src/hanzoai/types/organization/info_deprecated_params.py">params</a>) -> <a href="./src/hanzoai/types/organization/info_deprecated_response.py">object</a></code>
|
||||
- <code title="get /organization/info">client.organization.info.<a href="./pkg/hanzoai/resources/organization/info.py">retrieve</a>(\*\*<a href="pkg/hanzoai/types/organization/info_retrieve_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization_table_with_members.py">OrganizationTableWithMembers</a></code>
|
||||
- <code title="post /organization/info">client.organization.info.<a href="./pkg/hanzoai/resources/organization/info.py">deprecated</a>(\*\*<a href="pkg/hanzoai/types/organization/info_deprecated_params.py">params</a>) -> <a href="./pkg/hanzoai/types/organization/info_deprecated_response.py">object</a></code>
|
||||
|
||||
# Customer
|
||||
|
||||
@@ -895,39 +908,44 @@ Types:
|
||||
```python
|
||||
from hanzoai.types import (
|
||||
BlockUsers,
|
||||
HanzoEndUserTable,
|
||||
CustomerCreateResponse,
|
||||
CustomerUpdateResponse,
|
||||
CustomerListResponse,
|
||||
CustomerDeleteResponse,
|
||||
CustomerBlockResponse,
|
||||
CustomerRetrieveInfoResponse,
|
||||
CustomerUnblockResponse,
|
||||
)
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /customer/new">client.customer.<a href="./src/hanzoai/resources/customer.py">create</a>(\*\*<a href="src/hanzoai/types/customer_create_params.py">params</a>) -> <a href="./src/hanzoai/types/customer_create_response.py">object</a></code>
|
||||
- <code title="post /customer/update">client.customer.<a href="./src/hanzoai/resources/customer.py">update</a>(\*\*<a href="src/hanzoai/types/customer_update_params.py">params</a>) -> <a href="./src/hanzoai/types/customer_update_response.py">object</a></code>
|
||||
- <code title="get /customer/list">client.customer.<a href="./src/hanzoai/resources/customer.py">list</a>() -> <a href="./src/hanzoai/types/customer_list_response.py">CustomerListResponse</a></code>
|
||||
- <code title="post /customer/delete">client.customer.<a href="./src/hanzoai/resources/customer.py">delete</a>(\*\*<a href="src/hanzoai/types/customer_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/customer_delete_response.py">object</a></code>
|
||||
- <code title="post /customer/block">client.customer.<a href="./src/hanzoai/resources/customer.py">block</a>(\*\*<a href="src/hanzoai/types/customer_block_params.py">params</a>) -> <a href="./src/hanzoai/types/customer_block_response.py">object</a></code>
|
||||
- <code title="get /customer/info">client.customer.<a href="./src/hanzoai/resources/customer.py">retrieve_info</a>(\*\*<a href="src/hanzoai/types/customer_retrieve_info_params.py">params</a>) -> <a href="./src/hanzoai/types/customer_retrieve_info_response.py">CustomerRetrieveInfoResponse</a></code>
|
||||
- <code title="post /customer/unblock">client.customer.<a href="./src/hanzoai/resources/customer.py">unblock</a>(\*\*<a href="src/hanzoai/types/customer_unblock_params.py">params</a>) -> <a href="./src/hanzoai/types/customer_unblock_response.py">object</a></code>
|
||||
- <code title="post /customer/new">client.customer.<a href="./pkg/hanzoai/resources/customer.py">create</a>(\*\*<a href="pkg/hanzoai/types/customer_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/customer_create_response.py">object</a></code>
|
||||
- <code title="post /customer/update">client.customer.<a href="./pkg/hanzoai/resources/customer.py">update</a>(\*\*<a href="pkg/hanzoai/types/customer_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/customer_update_response.py">object</a></code>
|
||||
- <code title="get /customer/list">client.customer.<a href="./pkg/hanzoai/resources/customer.py">list</a>() -> <a href="./pkg/hanzoai/types/customer_list_response.py">CustomerListResponse</a></code>
|
||||
- <code title="post /customer/delete">client.customer.<a href="./pkg/hanzoai/resources/customer.py">delete</a>(\*\*<a href="pkg/hanzoai/types/customer_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/customer_delete_response.py">object</a></code>
|
||||
- <code title="post /customer/block">client.customer.<a href="./pkg/hanzoai/resources/customer.py">block</a>(\*\*<a href="pkg/hanzoai/types/customer_block_params.py">params</a>) -> <a href="./pkg/hanzoai/types/customer_block_response.py">object</a></code>
|
||||
- <code title="get /customer/info">client.customer.<a href="./pkg/hanzoai/resources/customer.py">retrieve_info</a>(\*\*<a href="pkg/hanzoai/types/customer_retrieve_info_params.py">params</a>) -> <a href="./pkg/hanzoai/types/hanzo_end_user_table.py">HanzoEndUserTable</a></code>
|
||||
- <code title="post /customer/unblock">client.customer.<a href="./pkg/hanzoai/resources/customer.py">unblock</a>(\*\*<a href="pkg/hanzoai/types/customer_unblock_params.py">params</a>) -> <a href="./pkg/hanzoai/types/customer_unblock_response.py">object</a></code>
|
||||
|
||||
# Spend
|
||||
|
||||
Types:
|
||||
|
||||
```python
|
||||
from hanzoai.types import SpendCalculateSpendResponse, SpendListLogsResponse, SpendListTagsResponse
|
||||
from hanzoai.types import (
|
||||
HanzoSpendLogs,
|
||||
SpendCalculateSpendResponse,
|
||||
SpendListLogsResponse,
|
||||
SpendListTagsResponse,
|
||||
)
|
||||
```
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /spend/calculate">client.spend.<a href="./src/hanzoai/resources/spend.py">calculate_spend</a>(\*\*<a href="src/hanzoai/types/spend_calculate_spend_params.py">params</a>) -> <a href="./src/hanzoai/types/spend_calculate_spend_response.py">object</a></code>
|
||||
- <code title="get /spend/logs">client.spend.<a href="./src/hanzoai/resources/spend.py">list_logs</a>(\*\*<a href="src/hanzoai/types/spend_list_logs_params.py">params</a>) -> <a href="./src/hanzoai/types/spend_list_logs_response.py">SpendListLogsResponse</a></code>
|
||||
- <code title="get /spend/tags">client.spend.<a href="./src/hanzoai/resources/spend.py">list_tags</a>(\*\*<a href="src/hanzoai/types/spend_list_tags_params.py">params</a>) -> <a href="./src/hanzoai/types/spend_list_tags_response.py">SpendListTagsResponse</a></code>
|
||||
- <code title="post /spend/calculate">client.spend.<a href="./pkg/hanzoai/resources/spend.py">calculate_spend</a>(\*\*<a href="pkg/hanzoai/types/spend_calculate_spend_params.py">params</a>) -> <a href="./pkg/hanzoai/types/spend_calculate_spend_response.py">object</a></code>
|
||||
- <code title="get /spend/logs">client.spend.<a href="./pkg/hanzoai/resources/spend.py">list_logs</a>(\*\*<a href="pkg/hanzoai/types/spend_list_logs_params.py">params</a>) -> <a href="./pkg/hanzoai/types/spend_list_logs_response.py">SpendListLogsResponse</a></code>
|
||||
- <code title="get /spend/tags">client.spend.<a href="./pkg/hanzoai/resources/spend.py">list_tags</a>(\*\*<a href="pkg/hanzoai/types/spend_list_tags_params.py">params</a>) -> <a href="./pkg/hanzoai/types/spend_list_tags_response.py">SpendListTagsResponse</a></code>
|
||||
|
||||
# Global
|
||||
|
||||
@@ -945,9 +963,9 @@ from hanzoai.types.global_ import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /global/spend/tags">client.global*.spend.<a href="./src/hanzoai/resources/global*/spend.py">list*tags</a>(\*\*<a href="src/hanzoai/types/global*/spend*list_tags_params.py">params</a>) -> <a href="./src/hanzoai/types/global*/spend_list_tags_response.py">SpendListTagsResponse</a></code>
|
||||
- <code title="post /global/spend/reset">client.global*.spend.<a href="./src/hanzoai/resources/global*/spend.py">reset</a>() -> <a href="./src/hanzoai/types/global_/spend_reset_response.py">object</a></code>
|
||||
- <code title="get /global/spend/report">client.global*.spend.<a href="./src/hanzoai/resources/global*/spend.py">retrieve*report</a>(\*\*<a href="src/hanzoai/types/global*/spend*retrieve_report_params.py">params</a>) -> <a href="./src/hanzoai/types/global*/spend_retrieve_report_response.py">SpendRetrieveReportResponse</a></code>
|
||||
- <code title="get /global/spend/tags">client.global*.spend.<a href="./pkg/hanzoai/resources/global*/spend.py">list*tags</a>(\*\*<a href="pkg/hanzoai/types/global*/spend*list_tags_params.py">params</a>) -> <a href="./pkg/hanzoai/types/global*/spend_list_tags_response.py">SpendListTagsResponse</a></code>
|
||||
- <code title="post /global/spend/reset">client.global*.spend.<a href="./pkg/hanzoai/resources/global*/spend.py">reset</a>() -> <a href="./pkg/hanzoai/types/global_/spend_reset_response.py">object</a></code>
|
||||
- <code title="get /global/spend/report">client.global*.spend.<a href="./pkg/hanzoai/resources/global*/spend.py">retrieve*report</a>(\*\*<a href="pkg/hanzoai/types/global*/spend*retrieve_report_params.py">params</a>) -> <a href="./pkg/hanzoai/types/global*/spend_retrieve_report_response.py">SpendRetrieveReportResponse</a></code>
|
||||
|
||||
# Provider
|
||||
|
||||
@@ -959,7 +977,7 @@ from hanzoai.types import ProviderListBudgetsResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /provider/budgets">client.provider.<a href="./src/hanzoai/resources/provider.py">list_budgets</a>() -> <a href="./src/hanzoai/types/provider_list_budgets_response.py">ProviderListBudgetsResponse</a></code>
|
||||
- <code title="get /provider/budgets">client.provider.<a href="./pkg/hanzoai/resources/provider.py">list_budgets</a>() -> <a href="./pkg/hanzoai/types/provider_list_budgets_response.py">ProviderListBudgetsResponse</a></code>
|
||||
|
||||
# Cache
|
||||
|
||||
@@ -971,9 +989,9 @@ from hanzoai.types import CacheDeleteResponse, CacheFlushAllResponse, CachePingR
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /cache/delete">client.cache.<a href="./src/hanzoai/resources/cache/cache.py">delete</a>() -> <a href="./src/hanzoai/types/cache_delete_response.py">object</a></code>
|
||||
- <code title="post /cache/flushall">client.cache.<a href="./src/hanzoai/resources/cache/cache.py">flush_all</a>() -> <a href="./src/hanzoai/types/cache_flush_all_response.py">object</a></code>
|
||||
- <code title="get /cache/ping">client.cache.<a href="./src/hanzoai/resources/cache/cache.py">ping</a>() -> <a href="./src/hanzoai/types/cache_ping_response.py">CachePingResponse</a></code>
|
||||
- <code title="post /cache/delete">client.cache.<a href="./pkg/hanzoai/resources/cache/cache.py">delete</a>() -> <a href="./pkg/hanzoai/types/cache_delete_response.py">object</a></code>
|
||||
- <code title="post /cache/flushall">client.cache.<a href="./pkg/hanzoai/resources/cache/cache.py">flush_all</a>() -> <a href="./pkg/hanzoai/types/cache_flush_all_response.py">object</a></code>
|
||||
- <code title="get /cache/ping">client.cache.<a href="./pkg/hanzoai/resources/cache/cache.py">ping</a>() -> <a href="./pkg/hanzoai/types/cache_ping_response.py">CachePingResponse</a></code>
|
||||
|
||||
## Redis
|
||||
|
||||
@@ -985,7 +1003,7 @@ from hanzoai.types.cache import RediRetrieveInfoResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /cache/redis/info">client.cache.redis.<a href="./src/hanzoai/resources/cache/redis.py">retrieve_info</a>() -> <a href="./src/hanzoai/types/cache/redi_retrieve_info_response.py">object</a></code>
|
||||
- <code title="get /cache/redis/info">client.cache.redis.<a href="./pkg/hanzoai/resources/cache/redis.py">retrieve_info</a>() -> <a href="./pkg/hanzoai/types/cache/redi_retrieve_info_response.py">object</a></code>
|
||||
|
||||
# Guardrails
|
||||
|
||||
@@ -997,7 +1015,7 @@ from hanzoai.types import GuardrailListResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /guardrails/list">client.guardrails.<a href="./src/hanzoai/resources/guardrails.py">list</a>() -> <a href="./src/hanzoai/types/guardrail_list_response.py">GuardrailListResponse</a></code>
|
||||
- <code title="get /guardrails/list">client.guardrails.<a href="./pkg/hanzoai/resources/guardrails.py">list</a>() -> <a href="./pkg/hanzoai/types/guardrail_list_response.py">GuardrailListResponse</a></code>
|
||||
|
||||
# Add
|
||||
|
||||
@@ -1009,7 +1027,7 @@ from hanzoai.types import IPAddress, AddAddAllowedIPResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /add/allowed_ip">client.add.<a href="./src/hanzoai/resources/add.py">add_allowed_ip</a>(\*\*<a href="src/hanzoai/types/add_add_allowed_ip_params.py">params</a>) -> <a href="./src/hanzoai/types/add_add_allowed_ip_response.py">object</a></code>
|
||||
- <code title="post /add/allowed_ip">client.add.<a href="./pkg/hanzoai/resources/add.py">add_allowed_ip</a>(\*\*<a href="pkg/hanzoai/types/add_add_allowed_ip_params.py">params</a>) -> <a href="./pkg/hanzoai/types/add_add_allowed_ip_response.py">object</a></code>
|
||||
|
||||
# Delete
|
||||
|
||||
@@ -1021,7 +1039,7 @@ from hanzoai.types import DeleteCreateAllowedIPResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /delete/allowed_ip">client.delete.<a href="./src/hanzoai/resources/delete.py">create_allowed_ip</a>(\*\*<a href="src/hanzoai/types/delete_create_allowed_ip_params.py">params</a>) -> <a href="./src/hanzoai/types/delete_create_allowed_ip_response.py">object</a></code>
|
||||
- <code title="post /delete/allowed_ip">client.delete.<a href="./pkg/hanzoai/resources/delete.py">create_allowed_ip</a>(\*\*<a href="pkg/hanzoai/types/delete_create_allowed_ip_params.py">params</a>) -> <a href="./pkg/hanzoai/types/delete_create_allowed_ip_response.py">object</a></code>
|
||||
|
||||
# Files
|
||||
|
||||
@@ -1038,10 +1056,10 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /{provider}/v1/files">client.files.<a href="./src/hanzoai/resources/files/files.py">create</a>(provider, \*\*<a href="src/hanzoai/types/file_create_params.py">params</a>) -> <a href="./src/hanzoai/types/file_create_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/files/{file_id}">client.files.<a href="./src/hanzoai/resources/files/files.py">retrieve</a>(file_id, \*, provider) -> <a href="./src/hanzoai/types/file_retrieve_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/files">client.files.<a href="./src/hanzoai/resources/files/files.py">list</a>(provider, \*\*<a href="src/hanzoai/types/file_list_params.py">params</a>) -> <a href="./src/hanzoai/types/file_list_response.py">object</a></code>
|
||||
- <code title="delete /{provider}/v1/files/{file_id}">client.files.<a href="./src/hanzoai/resources/files/files.py">delete</a>(file_id, \*, provider) -> <a href="./src/hanzoai/types/file_delete_response.py">object</a></code>
|
||||
- <code title="post /{provider}/v1/files">client.files.<a href="./pkg/hanzoai/resources/files/files.py">create</a>(provider, \*\*<a href="pkg/hanzoai/types/file_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/file_create_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/files/{file_id}">client.files.<a href="./pkg/hanzoai/resources/files/files.py">retrieve</a>(file_id, \*, provider) -> <a href="./pkg/hanzoai/types/file_retrieve_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/files">client.files.<a href="./pkg/hanzoai/resources/files/files.py">list</a>(provider, \*\*<a href="pkg/hanzoai/types/file_list_params.py">params</a>) -> <a href="./pkg/hanzoai/types/file_list_response.py">object</a></code>
|
||||
- <code title="delete /{provider}/v1/files/{file_id}">client.files.<a href="./pkg/hanzoai/resources/files/files.py">delete</a>(file_id, \*, provider) -> <a href="./pkg/hanzoai/types/file_delete_response.py">object</a></code>
|
||||
|
||||
## Content
|
||||
|
||||
@@ -1053,7 +1071,7 @@ from hanzoai.types.files import ContentRetrieveResponse
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /{provider}/v1/files/{file_id}/content">client.files.content.<a href="./src/hanzoai/resources/files/content.py">retrieve</a>(file_id, \*, provider) -> <a href="./src/hanzoai/types/files/content_retrieve_response.py">object</a></code>
|
||||
- <code title="get /{provider}/v1/files/{file_id}/content">client.files.content.<a href="./pkg/hanzoai/resources/files/content.py">retrieve</a>(file_id, \*, provider) -> <a href="./pkg/hanzoai/types/files/content_retrieve_response.py">object</a></code>
|
||||
|
||||
# Budget
|
||||
|
||||
@@ -1073,9 +1091,9 @@ from hanzoai.types import (
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /budget/new">client.budget.<a href="./src/hanzoai/resources/budget.py">create</a>(\*\*<a href="src/hanzoai/types/budget_create_params.py">params</a>) -> <a href="./src/hanzoai/types/budget_create_response.py">object</a></code>
|
||||
- <code title="post /budget/update">client.budget.<a href="./src/hanzoai/resources/budget.py">update</a>(\*\*<a href="src/hanzoai/types/budget_update_params.py">params</a>) -> <a href="./src/hanzoai/types/budget_update_response.py">object</a></code>
|
||||
- <code title="get /budget/list">client.budget.<a href="./src/hanzoai/resources/budget.py">list</a>() -> <a href="./src/hanzoai/types/budget_list_response.py">object</a></code>
|
||||
- <code title="post /budget/delete">client.budget.<a href="./src/hanzoai/resources/budget.py">delete</a>(\*\*<a href="src/hanzoai/types/budget_delete_params.py">params</a>) -> <a href="./src/hanzoai/types/budget_delete_response.py">object</a></code>
|
||||
- <code title="post /budget/info">client.budget.<a href="./src/hanzoai/resources/budget.py">info</a>(\*\*<a href="src/hanzoai/types/budget_info_params.py">params</a>) -> <a href="./src/hanzoai/types/budget_info_response.py">object</a></code>
|
||||
- <code title="get /budget/settings">client.budget.<a href="./src/hanzoai/resources/budget.py">settings</a>(\*\*<a href="src/hanzoai/types/budget_settings_params.py">params</a>) -> <a href="./src/hanzoai/types/budget_settings_response.py">object</a></code>
|
||||
- <code title="post /budget/new">client.budget.<a href="./pkg/hanzoai/resources/budget.py">create</a>(\*\*<a href="pkg/hanzoai/types/budget_create_params.py">params</a>) -> <a href="./pkg/hanzoai/types/budget_create_response.py">object</a></code>
|
||||
- <code title="post /budget/update">client.budget.<a href="./pkg/hanzoai/resources/budget.py">update</a>(\*\*<a href="pkg/hanzoai/types/budget_update_params.py">params</a>) -> <a href="./pkg/hanzoai/types/budget_update_response.py">object</a></code>
|
||||
- <code title="get /budget/list">client.budget.<a href="./pkg/hanzoai/resources/budget.py">list</a>() -> <a href="./pkg/hanzoai/types/budget_list_response.py">object</a></code>
|
||||
- <code title="post /budget/delete">client.budget.<a href="./pkg/hanzoai/resources/budget.py">delete</a>(\*\*<a href="pkg/hanzoai/types/budget_delete_params.py">params</a>) -> <a href="./pkg/hanzoai/types/budget_delete_response.py">object</a></code>
|
||||
- <code title="post /budget/info">client.budget.<a href="./pkg/hanzoai/resources/budget.py">info</a>(\*\*<a href="pkg/hanzoai/types/budget_info_params.py">params</a>) -> <a href="./pkg/hanzoai/types/budget_info_response.py">object</a></code>
|
||||
- <code title="get /budget/settings">client.budget.<a href="./pkg/hanzoai/resources/budget.py">settings</a>(\*\*<a href="pkg/hanzoai/types/budget_settings_params.py">params</a>) -> <a href="./pkg/hanzoai/types/budget_settings_response.py">object</a></code>
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# Hanzo AI SDK - Unified AI Features
|
||||
|
||||
The Hanzo AI SDK now includes integrated support for agents, MCP (Model Context Protocol), and local AI clusters, providing a complete solution for AI development that is **local, private, and free**.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hanzoai
|
||||
|
||||
# Optional: Install additional components
|
||||
pip install hanzo-agents # For agent networks
|
||||
pip install hanzo-mcp # For MCP tools
|
||||
pip install exo-explore # For local AI clusters
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Local AI Clusters
|
||||
|
||||
Run AI models locally on your own hardware or across your network of devices:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from hanzoai import cluster
|
||||
|
||||
async def run_local_ai():
|
||||
# Start a local cluster
|
||||
my_cluster = await cluster.start_local_cluster(
|
||||
name="my-ai-cluster",
|
||||
model_path="~/.cache/huggingface/hub"
|
||||
)
|
||||
|
||||
# Run inference locally
|
||||
result = await my_cluster.inference(
|
||||
prompt="Hello, local AI!",
|
||||
model="llama-3.2-3b"
|
||||
)
|
||||
|
||||
print(result)
|
||||
await my_cluster.stop()
|
||||
|
||||
asyncio.run(run_local_ai())
|
||||
```
|
||||
|
||||
### 2. Agent Networks
|
||||
|
||||
Create sophisticated AI agent systems with local and distributed execution:
|
||||
|
||||
```python
|
||||
from hanzoai import agents
|
||||
|
||||
# Create agents
|
||||
local_agent = agents.create_agent(
|
||||
name="local-assistant",
|
||||
model="llama-3.2-3b",
|
||||
base_url="http://localhost:8000" # Local cluster
|
||||
)
|
||||
|
||||
cloud_agent = agents.create_agent(
|
||||
name="cloud-assistant",
|
||||
model="anthropic/claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
# Create a network
|
||||
network = agents.create_network(
|
||||
agents=[local_agent, cloud_agent],
|
||||
router=agents.state_based_router() # Smart routing
|
||||
)
|
||||
|
||||
# The network automatically routes tasks to the best agent
|
||||
result = await network.run("Complex task requiring analysis")
|
||||
```
|
||||
|
||||
### 3. MCP Tools
|
||||
|
||||
Access 70+ tools through the Model Context Protocol:
|
||||
|
||||
```python
|
||||
from hanzoai import mcp
|
||||
|
||||
# Create an MCP server
|
||||
server = mcp.create_mcp_server(
|
||||
name="my-tools",
|
||||
allowed_paths=[".", "/workspace"],
|
||||
enable_agent_tool=True
|
||||
)
|
||||
|
||||
# Or connect to an existing MCP server
|
||||
client = mcp.MCPClient()
|
||||
await client.connect()
|
||||
|
||||
# Use tools
|
||||
result = await client.call_tool(
|
||||
"search",
|
||||
pattern="def main",
|
||||
path="."
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Mining Network
|
||||
|
||||
Contribute compute to the network and earn rewards:
|
||||
|
||||
```python
|
||||
from hanzoai import cluster
|
||||
|
||||
# Join the mining network
|
||||
miner = await cluster.join_mining_network(
|
||||
wallet_address="0x1234...",
|
||||
max_ram=8, # GB
|
||||
max_vram=4 # GB
|
||||
)
|
||||
|
||||
# Check mining stats
|
||||
stats = miner.get_stats()
|
||||
print(f"Compute contributed: {stats['compute_contributed']}")
|
||||
print(f"Rewards earned: {stats['rewards_earned']}")
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Local AI │ │ Agent Networks │ │ MCP Tools │
|
||||
│ Cluster │────▶│ │────▶│ │
|
||||
│ (exo-based) │ │ (hanzo-agents) │ │ (hanzo-mcp) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└───────────────────────┴────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Hanzo AI SDK │
|
||||
│ (unified API) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. **Cost-Effective Development**
|
||||
Use local models for development and testing, only using cloud APIs when necessary:
|
||||
|
||||
```python
|
||||
# Development: Use local cluster
|
||||
dev_result = await local_cluster.inference("Test prompt")
|
||||
|
||||
# Production: Use cloud with same interface
|
||||
prod_result = completion(model="gpt-4", messages=[...])
|
||||
```
|
||||
|
||||
### 2. **Privacy-First Applications**
|
||||
Keep sensitive data local while still leveraging AI:
|
||||
|
||||
```python
|
||||
# Process sensitive documents locally
|
||||
local_agent = agents.create_agent(
|
||||
name="privacy-agent",
|
||||
model="llama-3.2-3b",
|
||||
base_url="http://localhost:8000"
|
||||
)
|
||||
|
||||
result = await local_agent.process_documents(
|
||||
documents=sensitive_files,
|
||||
keep_local=True
|
||||
)
|
||||
```
|
||||
|
||||
### 3. **Distributed AI Workloads**
|
||||
Distribute work across multiple devices:
|
||||
|
||||
```python
|
||||
# Create a cluster across your devices
|
||||
cluster_config = cluster.ClusterConfig(
|
||||
broadcast_addresses=[
|
||||
"192.168.1.100", # Desktop
|
||||
"192.168.1.101", # Laptop
|
||||
"192.168.1.102", # Server
|
||||
]
|
||||
)
|
||||
|
||||
distributed_cluster = cluster.HanzoCluster(cluster_config)
|
||||
await distributed_cluster.start()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Local**: Always try local models first for cost savings
|
||||
2. **Smart Routing**: Use agent networks to automatically route between local and cloud
|
||||
3. **Cache Models**: Download and cache models locally for offline use
|
||||
4. **Join Mining**: Contribute unused compute to earn rewards
|
||||
5. **Use MCP**: Leverage the extensive tool ecosystem
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Local cluster configuration
|
||||
HANZO_CLUSTER_NAME=my-cluster
|
||||
HANZO_MODEL_PATH=~/.cache/huggingface/hub
|
||||
|
||||
# Mining configuration
|
||||
HANZO_WALLET_ADDRESS=0x1234...
|
||||
HANZO_MAX_RAM=16
|
||||
HANZO_MAX_VRAM=8
|
||||
|
||||
# Agent configuration
|
||||
HANZO_DEFAULT_MODEL=llama-3.2-3b
|
||||
HANZO_FALLBACK_MODEL=anthropic/claude-3-5-sonnet-20241022
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Automatic model downloading and management
|
||||
- [ ] Peer-to-peer model sharing
|
||||
- [ ] Federated learning support
|
||||
- [ ] Mobile device support
|
||||
- [ ] Enhanced mining rewards system
|
||||
- [ ] Native GUI for cluster management
|
||||
|
||||
## Support
|
||||
|
||||
For help and support:
|
||||
- Documentation: https://docs.hanzo.ai
|
||||
- Discord: https://discord.gg/hanzoai
|
||||
- GitHub: https://github.com/hanzoai
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example of using the unified Hanzo AI SDK.
|
||||
|
||||
This demonstrates:
|
||||
1. Local AI cluster with exo
|
||||
2. Agent networks
|
||||
3. MCP tools
|
||||
4. Cloud AI fallback
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from hanzoai import cluster, agents, mcp, completion
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the example."""
|
||||
|
||||
# 1. Start a local AI cluster
|
||||
print("Starting local AI cluster...")
|
||||
local_cluster = await cluster.start_local_cluster(
|
||||
name="my-cluster",
|
||||
model_path="~/.cache/huggingface/hub" # Use local models
|
||||
)
|
||||
|
||||
# 2. Create agents
|
||||
print("\nCreating AI agents...")
|
||||
|
||||
# Local agent using the cluster
|
||||
local_agent = agents.create_agent(
|
||||
name="local-helper",
|
||||
model="llama-3.2-3b", # Uses local cluster
|
||||
base_url=local_cluster.get_api_endpoint()
|
||||
)
|
||||
|
||||
# Cloud agent as fallback
|
||||
cloud_agent = agents.create_agent(
|
||||
name="cloud-helper",
|
||||
model="anthropic/claude-3-5-sonnet-20241022"
|
||||
)
|
||||
|
||||
# 3. Create an agent network
|
||||
print("\nCreating agent network...")
|
||||
network = agents.create_network(
|
||||
agents=[local_agent, cloud_agent],
|
||||
router=agents.state_based_router() # Smart routing
|
||||
)
|
||||
|
||||
# 4. Create MCP server with tools
|
||||
print("\nStarting MCP server...")
|
||||
mcp_server = mcp.create_mcp_server(
|
||||
name="hanzo-unified",
|
||||
allowed_paths=[".", "/tmp"],
|
||||
enable_agent_tool=True
|
||||
)
|
||||
|
||||
# 5. Example: Use local AI for simple tasks
|
||||
print("\n--- Local AI Example ---")
|
||||
local_result = await local_cluster.inference(
|
||||
prompt="Write a haiku about distributed AI",
|
||||
max_tokens=50
|
||||
)
|
||||
print(f"Local AI: {local_result}")
|
||||
|
||||
# 6. Example: Use cloud AI for complex tasks
|
||||
print("\n--- Cloud AI Example ---")
|
||||
cloud_result = completion(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": "Explain the benefits of local AI clusters"
|
||||
}]
|
||||
)
|
||||
print(f"Cloud AI: {cloud_result}")
|
||||
|
||||
# 7. Join mining network (optional)
|
||||
if input("\nJoin mining network? (y/n): ").lower() == 'y':
|
||||
wallet = input("Enter wallet address: ")
|
||||
miner = await cluster.join_mining_network(
|
||||
wallet_address=wallet
|
||||
)
|
||||
print(f"Mining stats: {miner.get_stats()}")
|
||||
|
||||
# Cleanup
|
||||
print("\nShutting down...")
|
||||
await local_cluster.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Hanzo AI Unified SDK Example ===")
|
||||
print("Local, Private, Free AI Infrastructure")
|
||||
print("=====================================\n")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -8,7 +8,7 @@ show_error_codes = True
|
||||
#
|
||||
# We also exclude our `tests` as mypy doesn't always infer
|
||||
# types correctly and Pyright will still catch any type errors.
|
||||
exclude = ^(src/hanzoai/_files\.py|_dev/.*\.py|tests/.*)$
|
||||
exclude = ^(pkg/hanzoai/_files\.py|_dev/.*\.py|tests/.*)$
|
||||
|
||||
strict_equality = True
|
||||
implicit_reexport = True
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,60 @@
|
||||
SHELL = /usr/bin/env bash
|
||||
|
||||
# Variables
|
||||
PYTHON_VERSION = 3.12
|
||||
VENV_DIR = .venv
|
||||
PRE_COMMIT_CONFIG = "./dev_config/python/.pre-commit-config.yaml"
|
||||
|
||||
# ANSI Color Codes
|
||||
GREEN := $(shell tput setaf 2)
|
||||
YELLOW := $(shell tput setaf 3)
|
||||
RED := $(shell tput setaf 1)
|
||||
BLUE := $(shell tput setaf 6)
|
||||
RESET := $(shell tput sgr0)
|
||||
|
||||
# Setup: Install Python (if needed), create virtualenv, and install project in editable mode.
|
||||
setup:
|
||||
@echo "$(YELLOW)Setting up environment...$(RESET)"
|
||||
@if ! command -v python$(PYTHON_VERSION) &>/dev/null; then \
|
||||
echo "$(BLUE)Installing Python $(PYTHON_VERSION) with uv...$(RESET)"; \
|
||||
uv python install $(PYTHON_VERSION); \
|
||||
fi
|
||||
@if [ ! -d "$(VENV_DIR)" ]; then \
|
||||
echo "$(BLUE)Creating virtual environment...$(RESET)"; \
|
||||
uv venv $(VENV_DIR) --python=$(PYTHON_VERSION); \
|
||||
fi
|
||||
@echo "$(BLUE)Installing project dependencies...$(RESET)"
|
||||
@. $(VENV_DIR)/bin/activate && uv pip install -e .
|
||||
@echo "$(GREEN)Setup complete.$(RESET)"
|
||||
|
||||
# Install pre-commit hooks
|
||||
install-pre-commit-hooks:
|
||||
@echo "$(YELLOW)Installing pre-commit hooks...$(RESET)"
|
||||
@git config --unset-all core.hooksPath || true
|
||||
@. $(VENV_DIR)/bin/activate && uv pip install pre-commit && pre-commit install --config $(PRE_COMMIT_CONFIG)
|
||||
@echo "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
|
||||
|
||||
# Lint Python code using pre-commit hooks
|
||||
lint-python:
|
||||
@echo "$(YELLOW)Running linters...$(RESET)"
|
||||
@. $(VENV_DIR)/bin/activate && pre-commit run --files dev_aci/**/* tests/**/* --show-diff-on-failure --config $(PRE_COMMIT_CONFIG)
|
||||
|
||||
lint: lint-python
|
||||
|
||||
# Build distribution packages using build (requires the 'build' package)
|
||||
build:
|
||||
@echo "$(YELLOW)Building distribution packages...$(RESET)"
|
||||
@. $(VENV_DIR)/bin/activate && uv pip install build && python -m build
|
||||
@echo "$(GREEN)Build complete. Distributions available in ./dist.$(RESET)"
|
||||
|
||||
# Publish packages to PyPI using twine
|
||||
publish: build
|
||||
@echo "$(YELLOW)Publishing packages to PyPI...$(RESET)"
|
||||
@. $(VENV_DIR)/bin/activate && uv pip install twine && python -m twine upload dist/*
|
||||
@echo "$(GREEN)Publish complete.$(RESET)"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "$(YELLOW)Cleaning build artifacts...$(RESET)"
|
||||
@rm -rf build dist *.egg-info
|
||||
@echo "$(GREEN)Clean complete.$(RESET)"
|
||||
@@ -0,0 +1,67 @@
|
||||
# Agent-Computer Interface (ACI) for Dev
|
||||
|
||||
An Agent-Computer Interface (ACI) designed for software development agents [Dev](https://github.com/hanzoai/Dev). This package provides essential tools and interfaces for AI agents to interact with computer systems for software development tasks.
|
||||
|
||||
## Features
|
||||
|
||||
- **Code Editor Interface**: Sophisticated editing capabilities through the `editor` module
|
||||
- File creation and modification
|
||||
- Code editing
|
||||
- Configuration management
|
||||
|
||||
- **Code Linting**: Built-in linting capabilities via the `linter` module
|
||||
- Tree-sitter based code analysis
|
||||
- Python-specific linting support
|
||||
|
||||
- **Utility Functions**: Helper modules for common operations
|
||||
- Shell command execution utilities
|
||||
- Diff generation and analysis
|
||||
- Logging functionality
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install dev-aci
|
||||
```
|
||||
|
||||
Or using Poetry:
|
||||
|
||||
```bash
|
||||
poetry add dev-aci
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
dev_aci/
|
||||
├── editor/ # Code editing functionality
|
||||
├── linter/ # Code linting capabilities
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/dev-aci.git
|
||||
cd dev-aci
|
||||
```
|
||||
|
||||
2. Install development dependencies:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Configure pre-commit-hooks
|
||||
```bash
|
||||
make install-pre-commit-hooks
|
||||
```
|
||||
|
||||
4. Run tests:
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -0,0 +1,100 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: dev-aci
|
||||
Version: 0.2.7
|
||||
Summary: An Agent-Computer Interface (ACI) designed for software development agents Dev.
|
||||
Author: Hanzo Industries Inc
|
||||
License: MIT
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: numpy
|
||||
Requires-Dist: pandas
|
||||
Requires-Dist: scipy
|
||||
Requires-Dist: networkx
|
||||
Requires-Dist: litellm
|
||||
Requires-Dist: gitpython
|
||||
Requires-Dist: tree-sitter>=0.24.0
|
||||
Requires-Dist: tree-sitter-python>=0.23.6
|
||||
Requires-Dist: tree-sitter-javascript>=0.23.1
|
||||
Requires-Dist: tree-sitter-typescript>=0.23.2
|
||||
Requires-Dist: tree-sitter-ruby>=0.23.1
|
||||
Requires-Dist: grep-ast==0.3.3
|
||||
Requires-Dist: flake8
|
||||
Requires-Dist: whatthepatch>=1.0.6
|
||||
Requires-Dist: binaryornot>=0.4.4
|
||||
Requires-Dist: cachetools>=5.5.2
|
||||
Requires-Dist: charset-normalizer>=3.4.1
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: ruff>=0.7.2; extra == "dev"
|
||||
Requires-Dist: pre-commit>=4.0.1; extra == "dev"
|
||||
Provides-Extra: test
|
||||
Requires-Dist: pytest>=8.3.3; extra == "test"
|
||||
Requires-Dist: psutil>=5.9.8; extra == "test"
|
||||
Dynamic: license-file
|
||||
|
||||
# Agent-Computer Interface (ACI) for Dev
|
||||
|
||||
An Agent-Computer Interface (ACI) designed for software development agents [Dev](https://github.com/hanzoai/Dev). This package provides essential tools and interfaces for AI agents to interact with computer systems for software development tasks.
|
||||
|
||||
## Features
|
||||
|
||||
- **Code Editor Interface**: Sophisticated editing capabilities through the `editor` module
|
||||
- File creation and modification
|
||||
- Code editing
|
||||
- Configuration management
|
||||
|
||||
- **Code Linting**: Built-in linting capabilities via the `linter` module
|
||||
- Tree-sitter based code analysis
|
||||
- Python-specific linting support
|
||||
|
||||
- **Utility Functions**: Helper modules for common operations
|
||||
- Shell command execution utilities
|
||||
- Diff generation and analysis
|
||||
- Logging functionality
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install dev-aci
|
||||
```
|
||||
|
||||
Or using Poetry:
|
||||
|
||||
```bash
|
||||
poetry add dev-aci
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
dev_aci/
|
||||
├── editor/ # Code editing functionality
|
||||
├── linter/ # Code linting capabilities
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/dev-aci.git
|
||||
cd dev-aci
|
||||
```
|
||||
|
||||
2. Install development dependencies:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Configure pre-commit-hooks
|
||||
```bash
|
||||
make install-pre-commit-hooks
|
||||
```
|
||||
|
||||
4. Run tests:
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -0,0 +1,27 @@
|
||||
LICENSE
|
||||
README.md
|
||||
pyproject.toml
|
||||
dev_aci/__init__.py
|
||||
dev_aci.egg-info/PKG-INFO
|
||||
dev_aci.egg-info/SOURCES.txt
|
||||
dev_aci.egg-info/dependency_links.txt
|
||||
dev_aci.egg-info/requires.txt
|
||||
dev_aci.egg-info/top_level.txt
|
||||
dev_aci/editor/__init__.py
|
||||
dev_aci/editor/config.py
|
||||
dev_aci/editor/editor.py
|
||||
dev_aci/editor/encoding.py
|
||||
dev_aci/editor/exceptions.py
|
||||
dev_aci/editor/file_cache.py
|
||||
dev_aci/editor/history.py
|
||||
dev_aci/editor/prompts.py
|
||||
dev_aci/editor/results.py
|
||||
dev_aci/linter/__init__.py
|
||||
dev_aci/linter/base.py
|
||||
dev_aci/linter/linter.py
|
||||
dev_aci/linter/impl/python.py
|
||||
dev_aci/linter/impl/treesitter.py
|
||||
dev_aci/linter/impl/treesitter_compat.py
|
||||
dev_aci/utils/diff.py
|
||||
dev_aci/utils/logger.py
|
||||
dev_aci/utils/shell.py
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
numpy
|
||||
pandas
|
||||
scipy
|
||||
networkx
|
||||
litellm
|
||||
gitpython
|
||||
tree-sitter>=0.24.0
|
||||
tree-sitter-python>=0.23.6
|
||||
tree-sitter-javascript>=0.23.1
|
||||
tree-sitter-typescript>=0.23.2
|
||||
tree-sitter-ruby>=0.23.1
|
||||
grep-ast==0.3.3
|
||||
flake8
|
||||
whatthepatch>=1.0.6
|
||||
binaryornot>=0.4.4
|
||||
cachetools>=5.5.2
|
||||
charset-normalizer>=3.4.1
|
||||
|
||||
[dev]
|
||||
ruff>=0.7.2
|
||||
pre-commit>=4.0.1
|
||||
|
||||
[test]
|
||||
pytest>=8.3.3
|
||||
psutil>=5.9.8
|
||||
@@ -0,0 +1 @@
|
||||
dev_aci
|
||||
@@ -0,0 +1,4 @@
|
||||
from .editor import file_editor
|
||||
from .editor.file_cache import FileCache
|
||||
|
||||
__all__ = ['file_editor', 'FileCache']
|
||||
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from .editor import Command, OHEditor
|
||||
from .encoding import EncodingManager, with_encoding
|
||||
from .exceptions import ToolError
|
||||
from .file_cache import FileCache
|
||||
from .results import ToolResult
|
||||
|
||||
_GLOBAL_EDITOR = OHEditor()
|
||||
|
||||
__all__ = [
|
||||
'Command',
|
||||
'OHEditor',
|
||||
'ToolError',
|
||||
'ToolResult',
|
||||
'FileCache',
|
||||
'file_editor',
|
||||
'EncodingManager',
|
||||
'with_encoding',
|
||||
]
|
||||
|
||||
|
||||
def _make_api_tool_result(tool_result: ToolResult) -> str:
|
||||
"""Convert an agent ToolResult to an API ToolResultBlockParam."""
|
||||
if tool_result.error:
|
||||
return f'ERROR:\n{tool_result.error}'
|
||||
|
||||
assert tool_result.output, 'Expected output in file_editor.'
|
||||
return tool_result.output
|
||||
|
||||
|
||||
def file_editor(
|
||||
command: Command,
|
||||
path: str,
|
||||
file_text: str | None = None,
|
||||
view_range: list[int] | None = None,
|
||||
old_str: str | None = None,
|
||||
new_str: str | None = None,
|
||||
insert_line: int | None = None,
|
||||
enable_linting: bool = False,
|
||||
) -> str:
|
||||
result: ToolResult | None = None
|
||||
try:
|
||||
result = _GLOBAL_EDITOR(
|
||||
command=command,
|
||||
path=path,
|
||||
file_text=file_text,
|
||||
view_range=view_range,
|
||||
old_str=old_str,
|
||||
new_str=new_str,
|
||||
insert_line=insert_line,
|
||||
enable_linting=enable_linting,
|
||||
)
|
||||
except ToolError as e:
|
||||
result = ToolResult(error=e.message)
|
||||
|
||||
formatted_output_and_error = _make_api_tool_result(result)
|
||||
marker_id = uuid.uuid4().hex
|
||||
|
||||
def json_generator():
|
||||
yield '{'
|
||||
first = True
|
||||
for key, value in result.to_dict().items():
|
||||
if not first:
|
||||
yield ','
|
||||
first = False
|
||||
yield f'"{key}": {json.dumps(value)}'
|
||||
yield f', "formatted_output_and_error": {json.dumps(formatted_output_and_error)}'
|
||||
yield '}'
|
||||
|
||||
return (
|
||||
f'<oh_aci_output_{marker_id}>\n'
|
||||
+ ''.join(json_generator())
|
||||
+ f'\n</oh_aci_output_{marker_id}>'
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
MAX_RESPONSE_LEN_CHAR: int = 16000
|
||||
SNIPPET_CONTEXT_WINDOW: int = 4
|
||||
@@ -0,0 +1,659 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Literal, get_args
|
||||
|
||||
from binaryornot.check import is_binary
|
||||
|
||||
from dev_aci.linter import DefaultLinter
|
||||
from dev_aci.utils.shell import run_shell_cmd
|
||||
|
||||
from .config import SNIPPET_CONTEXT_WINDOW
|
||||
from .encoding import EncodingManager, with_encoding
|
||||
from .exceptions import (
|
||||
EditorToolParameterInvalidError,
|
||||
EditorToolParameterMissingError,
|
||||
FileValidationError,
|
||||
ToolError,
|
||||
)
|
||||
from .history import FileHistoryManager
|
||||
from .prompts import DIRECTORY_CONTENT_TRUNCATED_NOTICE, FILE_CONTENT_TRUNCATED_NOTICE
|
||||
from .results import CLIResult, maybe_truncate
|
||||
|
||||
Command = Literal[
|
||||
'view',
|
||||
'create',
|
||||
'str_replace',
|
||||
'insert',
|
||||
'undo_edit',
|
||||
# 'jump_to_definition', TODO:
|
||||
# 'find_references' TODO:
|
||||
]
|
||||
|
||||
|
||||
class OHEditor:
|
||||
"""
|
||||
An filesystem editor tool that allows the agent to
|
||||
- view
|
||||
- create
|
||||
- navigate
|
||||
- edit files
|
||||
The tool parameters are defined by Anthropic and are not editable.
|
||||
|
||||
Original implementation: https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/edit.py
|
||||
"""
|
||||
|
||||
TOOL_NAME = 'oh_editor'
|
||||
MAX_FILE_SIZE_MB = 10 # Maximum file size in MB
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_file_size_mb: int | None = None,
|
||||
workspace_root: str | None = None,
|
||||
):
|
||||
"""Initialize the editor.
|
||||
|
||||
Args:
|
||||
max_file_size_mb: Maximum file size in MB. If None, uses the default MAX_FILE_SIZE_MB.
|
||||
workspace_root: Root directory that serves as the current working directory for relative path
|
||||
suggestions. Must be an absolute path. If None, no path suggestions will be
|
||||
provided for relative paths.
|
||||
workspace_root: Root directory that serves as the current working directory for relative path
|
||||
suggestions. Must be an absolute path. If None, no path suggestions will be
|
||||
provided for relative paths.
|
||||
"""
|
||||
self._linter = DefaultLinter()
|
||||
self._history_manager = FileHistoryManager(max_history_per_file=10)
|
||||
self._max_file_size = (
|
||||
(max_file_size_mb or self.MAX_FILE_SIZE_MB) * 1024 * 1024
|
||||
) # Convert to bytes
|
||||
# Initialize encoding manager
|
||||
self._encoding_manager = EncodingManager()
|
||||
# Set cwd (current working directory) if workspace_root is provided
|
||||
if workspace_root is not None:
|
||||
workspace_path = Path(workspace_root)
|
||||
# Ensure workspace_root is an absolute path
|
||||
if not workspace_path.is_absolute():
|
||||
raise ValueError(
|
||||
f'workspace_root must be an absolute path, got: {workspace_root}'
|
||||
)
|
||||
self._cwd = workspace_path
|
||||
else:
|
||||
self._cwd = None # type: ignore
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
command: Command,
|
||||
path: str,
|
||||
file_text: str | None = None,
|
||||
view_range: list[int] | None = None,
|
||||
old_str: str | None = None,
|
||||
new_str: str | None = None,
|
||||
insert_line: int | None = None,
|
||||
enable_linting: bool = False,
|
||||
**kwargs,
|
||||
) -> CLIResult:
|
||||
_path = Path(path)
|
||||
self.validate_path(command, _path)
|
||||
if command == 'view':
|
||||
return self.view(_path, view_range)
|
||||
elif command == 'create':
|
||||
if file_text is None:
|
||||
raise EditorToolParameterMissingError(command, 'file_text')
|
||||
self.write_file(_path, file_text)
|
||||
self._history_manager.add_history(_path, file_text)
|
||||
return CLIResult(
|
||||
path=str(_path),
|
||||
new_content=file_text,
|
||||
prev_exist=False,
|
||||
output=f'File created successfully at: {_path}',
|
||||
)
|
||||
elif command == 'str_replace':
|
||||
if old_str is None:
|
||||
raise EditorToolParameterMissingError(command, 'old_str')
|
||||
if new_str == old_str:
|
||||
raise EditorToolParameterInvalidError(
|
||||
'new_str',
|
||||
new_str,
|
||||
'No replacement was performed. `new_str` and `old_str` must be different.',
|
||||
)
|
||||
return self.str_replace(_path, old_str, new_str, enable_linting)
|
||||
elif command == 'insert':
|
||||
if insert_line is None:
|
||||
raise EditorToolParameterMissingError(command, 'insert_line')
|
||||
if new_str is None:
|
||||
raise EditorToolParameterMissingError(command, 'new_str')
|
||||
return self.insert(_path, insert_line, new_str, enable_linting)
|
||||
elif command == 'undo_edit':
|
||||
return self.undo_edit(_path)
|
||||
|
||||
raise ToolError(
|
||||
f'Unrecognized command {command}. The allowed commands for the {self.TOOL_NAME} tool are: {", ".join(get_args(Command))}'
|
||||
)
|
||||
|
||||
@with_encoding
|
||||
def _count_lines(self, path: Path, encoding: str = 'utf-8') -> int:
|
||||
"""
|
||||
Count the number of lines in a file safely.
|
||||
|
||||
Args:
|
||||
path: Path to the file
|
||||
encoding: The encoding to use when reading the file (auto-detected by decorator)
|
||||
|
||||
Returns:
|
||||
The number of lines in the file
|
||||
"""
|
||||
with open(path, encoding=encoding) as f:
|
||||
return sum(1 for _ in f)
|
||||
|
||||
@with_encoding
|
||||
def str_replace(
|
||||
self,
|
||||
path: Path,
|
||||
old_str: str,
|
||||
new_str: str | None,
|
||||
enable_linting: bool,
|
||||
encoding: str = 'utf-8',
|
||||
) -> CLIResult:
|
||||
"""
|
||||
Implement the str_replace command, which replaces old_str with new_str in the file content.
|
||||
|
||||
Args:
|
||||
path: Path to the file
|
||||
old_str: String to replace
|
||||
new_str: Replacement string
|
||||
enable_linting: Whether to run linting on the changes
|
||||
encoding: The encoding to use (auto-detected by decorator)
|
||||
"""
|
||||
self.validate_file(path)
|
||||
old_str = old_str.expandtabs()
|
||||
new_str = new_str.expandtabs() if new_str is not None else ''
|
||||
|
||||
# Read the entire file first to handle both single-line and multi-line replacements
|
||||
file_content = self.read_file(path).expandtabs()
|
||||
|
||||
# Find all occurrences using regex
|
||||
# Escape special regex characters in old_str to match it literally
|
||||
pattern = re.escape(old_str)
|
||||
occurrences = [
|
||||
(
|
||||
file_content.count('\n', 0, match.start()) + 1, # line number
|
||||
match.group(), # matched text
|
||||
match.start(), # start position
|
||||
)
|
||||
for match in re.finditer(pattern, file_content)
|
||||
]
|
||||
|
||||
if not occurrences:
|
||||
raise ToolError(
|
||||
f'No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}.'
|
||||
)
|
||||
if len(occurrences) > 1:
|
||||
line_numbers = sorted(set(line for line, _, _ in occurrences))
|
||||
raise ToolError(
|
||||
f'No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {line_numbers}. Please ensure it is unique.'
|
||||
)
|
||||
|
||||
# We found exactly one occurrence
|
||||
replacement_line, matched_text, idx = occurrences[0]
|
||||
|
||||
# Create new content by replacing just the matched text
|
||||
new_file_content = (
|
||||
file_content[:idx] + new_str + file_content[idx + len(matched_text) :]
|
||||
)
|
||||
|
||||
# Write the new content to the file
|
||||
self.write_file(path, new_file_content)
|
||||
|
||||
# Save the content to history
|
||||
self._history_manager.add_history(path, file_content)
|
||||
|
||||
# Create a snippet of the edited section
|
||||
start_line = max(0, replacement_line - SNIPPET_CONTEXT_WINDOW)
|
||||
end_line = replacement_line + SNIPPET_CONTEXT_WINDOW + new_str.count('\n')
|
||||
|
||||
# Read just the snippet range
|
||||
snippet = self.read_file(path, start_line=start_line, end_line=end_line)
|
||||
|
||||
# Prepare the success message
|
||||
success_message = f'The file {path} has been edited. '
|
||||
success_message += self._make_output(
|
||||
snippet, f'a snippet of {path}', start_line + 1
|
||||
)
|
||||
|
||||
if enable_linting:
|
||||
# Run linting on the changes
|
||||
lint_results = self._run_linting(file_content, new_file_content, path)
|
||||
success_message += '\n' + lint_results + '\n'
|
||||
|
||||
success_message += 'Review the changes and make sure they are as expected. Edit the file again if necessary.'
|
||||
return CLIResult(
|
||||
output=success_message,
|
||||
prev_exist=True,
|
||||
path=str(path),
|
||||
old_content=file_content,
|
||||
new_content=new_file_content,
|
||||
)
|
||||
|
||||
def view(self, path: Path, view_range: list[int] | None = None) -> CLIResult:
|
||||
"""
|
||||
View the contents of a file or a directory.
|
||||
"""
|
||||
if path.is_dir():
|
||||
if view_range:
|
||||
raise EditorToolParameterInvalidError(
|
||||
'view_range',
|
||||
view_range,
|
||||
'The `view_range` parameter is not allowed when `path` points to a directory.',
|
||||
)
|
||||
|
||||
# First count hidden files/dirs in current directory only
|
||||
# -mindepth 1 excludes . and .. automatically
|
||||
_, hidden_stdout, _ = run_shell_cmd(
|
||||
rf"find -L {path} -mindepth 1 -maxdepth 1 -name '.*'"
|
||||
)
|
||||
hidden_count = (
|
||||
len(hidden_stdout.strip().split('\n')) if hidden_stdout.strip() else 0
|
||||
)
|
||||
|
||||
# Then get files/dirs up to 2 levels deep, excluding hidden entries at both depth 1 and 2
|
||||
_, stdout, stderr = run_shell_cmd(
|
||||
rf"find -L {path} -maxdepth 2 -not \( -path '{path}/\.*' -o -path '{path}/*/\.*' \) | sort",
|
||||
truncate_notice=DIRECTORY_CONTENT_TRUNCATED_NOTICE,
|
||||
)
|
||||
if not stderr:
|
||||
# Add trailing slashes to directories
|
||||
paths = stdout.strip().split('\n') if stdout.strip() else []
|
||||
formatted_paths = []
|
||||
for p in paths:
|
||||
if Path(p).is_dir():
|
||||
formatted_paths.append(f'{p}/')
|
||||
else:
|
||||
formatted_paths.append(p)
|
||||
|
||||
msg = [
|
||||
f"Here's the files and directories up to 2 levels deep in {path}, excluding hidden items:\n"
|
||||
+ '\n'.join(formatted_paths)
|
||||
]
|
||||
if hidden_count > 0:
|
||||
msg.append(
|
||||
f"\n{hidden_count} hidden files/directories in this directory are excluded. You can use 'ls -la {path}' to see them."
|
||||
)
|
||||
stdout = '\n'.join(msg)
|
||||
return CLIResult(
|
||||
output=stdout,
|
||||
error=stderr,
|
||||
path=str(path),
|
||||
prev_exist=True,
|
||||
)
|
||||
|
||||
# Validate file and count lines
|
||||
self.validate_file(path)
|
||||
num_lines = self._count_lines(path)
|
||||
|
||||
start_line = 1
|
||||
if not view_range:
|
||||
file_content = self.read_file(path)
|
||||
output = self._make_output(file_content, str(path), start_line)
|
||||
|
||||
return CLIResult(
|
||||
output=output,
|
||||
path=str(path),
|
||||
prev_exist=True,
|
||||
)
|
||||
|
||||
if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range):
|
||||
raise EditorToolParameterInvalidError(
|
||||
'view_range',
|
||||
view_range,
|
||||
'It should be a list of two integers.',
|
||||
)
|
||||
|
||||
start_line, end_line = view_range
|
||||
if start_line < 1 or start_line > num_lines:
|
||||
raise EditorToolParameterInvalidError(
|
||||
'view_range',
|
||||
view_range,
|
||||
f'Its first element `{start_line}` should be within the range of lines of the file: {[1, num_lines]}.',
|
||||
)
|
||||
|
||||
if end_line > num_lines:
|
||||
raise EditorToolParameterInvalidError(
|
||||
'view_range',
|
||||
view_range,
|
||||
f'Its second element `{end_line}` should be smaller than the number of lines in the file: `{num_lines}`.',
|
||||
)
|
||||
|
||||
if end_line != -1 and end_line < start_line:
|
||||
raise EditorToolParameterInvalidError(
|
||||
'view_range',
|
||||
view_range,
|
||||
f'Its second element `{end_line}` should be greater than or equal to the first element `{start_line}`.',
|
||||
)
|
||||
|
||||
if end_line == -1:
|
||||
end_line = num_lines
|
||||
|
||||
file_content = self.read_file(path, start_line=start_line, end_line=end_line)
|
||||
|
||||
# Get the detected encoding
|
||||
output = self._make_output(file_content, str(path), start_line)
|
||||
|
||||
return CLIResult(
|
||||
path=str(path),
|
||||
output=output,
|
||||
prev_exist=True,
|
||||
)
|
||||
|
||||
@with_encoding
|
||||
def write_file(self, path: Path, file_text: str, encoding: str = 'utf-8') -> None:
|
||||
"""
|
||||
Write the content of a file to a given path; raise a ToolError if an error occurs.
|
||||
|
||||
Args:
|
||||
path: Path to the file to write
|
||||
file_text: Content to write to the file
|
||||
encoding: The encoding to use when writing the file (auto-detected by decorator)
|
||||
"""
|
||||
self.validate_file(path)
|
||||
try:
|
||||
# Use open with encoding instead of path.write_text
|
||||
with open(path, 'w', encoding=encoding) as f:
|
||||
f.write(file_text)
|
||||
except Exception as e:
|
||||
raise ToolError(f'Ran into {e} while trying to write to {path}') from None
|
||||
|
||||
@with_encoding
|
||||
def insert(
|
||||
self,
|
||||
path: Path,
|
||||
insert_line: int,
|
||||
new_str: str,
|
||||
enable_linting: bool,
|
||||
encoding: str = 'utf-8',
|
||||
) -> CLIResult:
|
||||
"""
|
||||
Implement the insert command, which inserts new_str at the specified line in the file content.
|
||||
|
||||
Args:
|
||||
path: Path to the file
|
||||
insert_line: Line number where to insert the new content
|
||||
new_str: Content to insert
|
||||
enable_linting: Whether to run linting on the changes
|
||||
encoding: The encoding to use (auto-detected by decorator)
|
||||
"""
|
||||
# Validate file and count lines
|
||||
self.validate_file(path)
|
||||
num_lines = self._count_lines(path)
|
||||
|
||||
if insert_line < 0 or insert_line > num_lines:
|
||||
raise EditorToolParameterInvalidError(
|
||||
'insert_line',
|
||||
insert_line,
|
||||
f'It should be within the range of lines of the file: {[0, num_lines]}',
|
||||
)
|
||||
|
||||
new_str = new_str.expandtabs()
|
||||
new_str_lines = new_str.split('\n')
|
||||
|
||||
# Create temporary file for the new content
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w', encoding=encoding, delete=False
|
||||
) as temp_file:
|
||||
# Copy lines before insert point and save them for history
|
||||
history_lines = []
|
||||
with open(path, 'r', encoding=encoding) as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
if i > insert_line:
|
||||
break
|
||||
temp_file.write(line.expandtabs())
|
||||
history_lines.append(line)
|
||||
|
||||
# Insert new content
|
||||
for line in new_str_lines:
|
||||
temp_file.write(line + '\n')
|
||||
|
||||
# Copy remaining lines and save them for history
|
||||
with open(path, 'r', encoding=encoding) as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
if i <= insert_line:
|
||||
continue
|
||||
temp_file.write(line.expandtabs())
|
||||
history_lines.append(line)
|
||||
|
||||
# Move temporary file to original location
|
||||
shutil.move(temp_file.name, path)
|
||||
|
||||
# Read just the snippet range
|
||||
start_line = max(1, insert_line - SNIPPET_CONTEXT_WINDOW)
|
||||
end_line = min(
|
||||
num_lines + len(new_str_lines),
|
||||
insert_line + SNIPPET_CONTEXT_WINDOW + len(new_str_lines),
|
||||
)
|
||||
snippet = self.read_file(path, start_line=start_line, end_line=end_line)
|
||||
|
||||
# Save history - we already have the lines in memory
|
||||
file_text = ''.join(history_lines)
|
||||
self._history_manager.add_history(path, file_text)
|
||||
|
||||
# Read new content for result
|
||||
new_file_text = self.read_file(path)
|
||||
|
||||
success_message = f'The file {path} has been edited. '
|
||||
success_message += self._make_output(
|
||||
snippet,
|
||||
'a snippet of the edited file',
|
||||
max(1, insert_line - SNIPPET_CONTEXT_WINDOW + 1),
|
||||
)
|
||||
|
||||
if enable_linting:
|
||||
# Run linting on the changes
|
||||
lint_results = self._run_linting(file_text, new_file_text, path)
|
||||
success_message += '\n' + lint_results + '\n'
|
||||
|
||||
success_message += 'Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary.'
|
||||
return CLIResult(
|
||||
output=success_message,
|
||||
prev_exist=True,
|
||||
path=str(path),
|
||||
old_content=file_text,
|
||||
new_content=new_file_text,
|
||||
)
|
||||
|
||||
def validate_path(self, command: Command, path: Path) -> None:
|
||||
"""
|
||||
Check that the path/command combination is valid.
|
||||
|
||||
Validates:
|
||||
1. Path is absolute
|
||||
2. Path and command are compatible
|
||||
|
||||
Validates:
|
||||
1. Path is absolute
|
||||
2. Path and command are compatible
|
||||
"""
|
||||
# Check if its an absolute path
|
||||
if not path.is_absolute():
|
||||
suggestion_message = (
|
||||
'The path should be an absolute path, starting with `/`.'
|
||||
)
|
||||
|
||||
# Only suggest the absolute path if cwd is provided and the path exists
|
||||
if self._cwd is not None:
|
||||
suggested_path = self._cwd / path
|
||||
if suggested_path.exists():
|
||||
suggestion_message += f' Maybe you meant {suggested_path}?'
|
||||
|
||||
raise EditorToolParameterInvalidError(
|
||||
'path',
|
||||
path,
|
||||
suggestion_message,
|
||||
)
|
||||
|
||||
# Check if path and command are compatible
|
||||
if command == 'create' and path.exists():
|
||||
raise EditorToolParameterInvalidError(
|
||||
'path',
|
||||
path,
|
||||
f'File already exists at: {path}. Cannot overwrite files using command `create`.',
|
||||
)
|
||||
if command != 'create' and not path.exists():
|
||||
raise EditorToolParameterInvalidError(
|
||||
'path',
|
||||
path,
|
||||
f'The path {path} does not exist. Please provide a valid path.',
|
||||
)
|
||||
if command != 'view' and path.is_dir():
|
||||
raise EditorToolParameterInvalidError(
|
||||
'path',
|
||||
path,
|
||||
f'The path {path} is a directory and only the `view` command can be used on directories.',
|
||||
)
|
||||
|
||||
def undo_edit(self, path: Path) -> CLIResult:
|
||||
"""
|
||||
Implement the undo_edit command.
|
||||
"""
|
||||
current_text = self.read_file(path).expandtabs()
|
||||
old_text = self._history_manager.pop_last_history(path)
|
||||
if old_text is None:
|
||||
raise ToolError(f'No edit history found for {path}.')
|
||||
|
||||
self.write_file(path, old_text)
|
||||
|
||||
return CLIResult(
|
||||
output=f'Last edit to {path} undone successfully. {self._make_output(old_text, str(path))}',
|
||||
path=str(path),
|
||||
prev_exist=True,
|
||||
old_content=current_text,
|
||||
new_content=old_text,
|
||||
)
|
||||
|
||||
def validate_file(self, path: Path) -> None:
|
||||
"""
|
||||
Validate a file for reading or editing operations.
|
||||
|
||||
Args:
|
||||
path: Path to the file to validate
|
||||
|
||||
Raises:
|
||||
FileValidationError: If the file fails validation
|
||||
"""
|
||||
# Skip validation for directories or non-existent files (for create command)
|
||||
if not path.exists() or not path.is_file():
|
||||
return
|
||||
|
||||
# Check file size
|
||||
file_size = os.path.getsize(path)
|
||||
max_size = self._max_file_size
|
||||
if file_size > max_size:
|
||||
raise FileValidationError(
|
||||
path=str(path),
|
||||
reason=f'File is too large ({file_size / 1024 / 1024:.1f}MB). Maximum allowed size is {int(max_size / 1024 / 1024)}MB.',
|
||||
)
|
||||
|
||||
# Check file type
|
||||
if is_binary(str(path)):
|
||||
raise FileValidationError(
|
||||
path=str(path),
|
||||
reason='File appears to be binary. Only text files can be edited.',
|
||||
)
|
||||
|
||||
@with_encoding
|
||||
def read_file(
|
||||
self,
|
||||
path: Path,
|
||||
start_line: int | None = None,
|
||||
end_line: int | None = None,
|
||||
encoding: str = 'utf-8', # Default will be overridden by decorator
|
||||
) -> str:
|
||||
"""
|
||||
Read the content of a file from a given path; raise a ToolError if an error occurs.
|
||||
|
||||
Args:
|
||||
path: Path to the file to read
|
||||
start_line: Optional start line number (1-based). If provided with end_line, only reads that range.
|
||||
end_line: Optional end line number (1-based). Must be provided with start_line.
|
||||
encoding: The encoding to use when reading the file (auto-detected by decorator)
|
||||
"""
|
||||
self.validate_file(path)
|
||||
try:
|
||||
if start_line is not None and end_line is not None:
|
||||
# Read only the specified line range
|
||||
lines = []
|
||||
with open(path, 'r', encoding=encoding) as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
if i > end_line:
|
||||
break
|
||||
if i >= start_line:
|
||||
lines.append(line)
|
||||
return ''.join(lines)
|
||||
elif start_line is not None or end_line is not None:
|
||||
raise ValueError(
|
||||
'Both start_line and end_line must be provided together'
|
||||
)
|
||||
else:
|
||||
# Use line-by-line reading to avoid loading entire file into memory
|
||||
with open(path, 'r', encoding=encoding) as f:
|
||||
return ''.join(f)
|
||||
except Exception as e:
|
||||
raise ToolError(f'Ran into {e} while trying to read {path}') from None
|
||||
|
||||
def _make_output(
|
||||
self,
|
||||
snippet_content: str,
|
||||
snippet_description: str,
|
||||
start_line: int = 1,
|
||||
expand_tabs: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Generate output for the CLI based on the content of a code snippet.
|
||||
"""
|
||||
snippet_content = maybe_truncate(
|
||||
snippet_content, truncate_notice=FILE_CONTENT_TRUNCATED_NOTICE
|
||||
)
|
||||
if expand_tabs:
|
||||
snippet_content = snippet_content.expandtabs()
|
||||
|
||||
snippet_content = '\n'.join(
|
||||
[
|
||||
f'{i + start_line:6}\t{line}'
|
||||
for i, line in enumerate(snippet_content.split('\n'))
|
||||
]
|
||||
)
|
||||
return (
|
||||
f"Here's the result of running `cat -n` on {snippet_description}:\n"
|
||||
+ snippet_content
|
||||
+ '\n'
|
||||
)
|
||||
|
||||
def _run_linting(self, old_content: str, new_content: str, path: Path) -> str:
|
||||
"""
|
||||
Run linting on file changes and return formatted results.
|
||||
"""
|
||||
# Create a temporary directory
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create paths with exact filenames in temp directory
|
||||
temp_old = Path(temp_dir) / f'old.{path.name}'
|
||||
temp_new = Path(temp_dir) / f'new.{path.name}'
|
||||
|
||||
# Write content to temporary files
|
||||
temp_old.write_text(old_content)
|
||||
temp_new.write_text(new_content)
|
||||
|
||||
# Run linting on the changes
|
||||
results = self._linter.lint_file_diff(str(temp_old), str(temp_new))
|
||||
|
||||
if not results:
|
||||
return 'No linting issues found in the changes.'
|
||||
|
||||
# Format results
|
||||
output = ['Linting issues found in the changes:']
|
||||
for result in results:
|
||||
output.append(
|
||||
f'- Line {result.line}, Column {result.column}: {result.message}'
|
||||
)
|
||||
return '\n'.join(output) + '\n'
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Encoding management for file operations."""
|
||||
|
||||
import functools
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import charset_normalizer
|
||||
from cachetools import LRUCache
|
||||
|
||||
|
||||
class EncodingManager:
|
||||
"""Manages file encodings across multiple operations to ensure consistency."""
|
||||
|
||||
# Default maximum number of entries in the cache
|
||||
DEFAULT_MAX_CACHE_SIZE = 1000 # ~= 300 KB
|
||||
|
||||
def __init__(self, max_cache_size=None):
|
||||
# Cache detected encodings to avoid repeated detection on the same file
|
||||
# Format: {path_str: (encoding, mtime)}
|
||||
self._encoding_cache: LRUCache[str, Tuple[str, float]] = LRUCache(
|
||||
maxsize=max_cache_size or self.DEFAULT_MAX_CACHE_SIZE
|
||||
)
|
||||
# Default fallback encoding
|
||||
self.default_encoding = 'utf-8'
|
||||
# Confidence threshold for encoding detection
|
||||
self.confidence_threshold = 0.9
|
||||
|
||||
def detect_encoding(self, path: Path) -> str:
|
||||
"""Detect the encoding of a file without handling caching logic.
|
||||
Args:
|
||||
path: Path to the file
|
||||
Returns:
|
||||
The detected encoding or default encoding if detection fails
|
||||
"""
|
||||
# Handle non-existent files
|
||||
if not path.exists():
|
||||
return self.default_encoding
|
||||
|
||||
# Read a sample of the file to detect encoding
|
||||
sample_size = min(os.path.getsize(path), 1024 * 1024) # Max 1MB sample
|
||||
with open(path, 'rb') as f:
|
||||
raw_data = f.read(sample_size)
|
||||
|
||||
# Use charset_normalizer instead of chardet
|
||||
results = charset_normalizer.detect(raw_data)
|
||||
|
||||
# Get the best match if any exists
|
||||
if results and results['confidence'] > self.confidence_threshold:
|
||||
encoding = results['encoding']
|
||||
else:
|
||||
encoding = self.default_encoding
|
||||
|
||||
return encoding
|
||||
|
||||
def get_encoding(self, path: Path) -> str:
|
||||
"""Get encoding for a file, using cache or detecting if necessary.
|
||||
Args:
|
||||
path: Path to the file
|
||||
Returns:
|
||||
The encoding for the file
|
||||
"""
|
||||
path_str = str(path)
|
||||
# If file doesn't exist, return default encoding
|
||||
if not path.exists():
|
||||
return self.default_encoding
|
||||
|
||||
# Get current modification time
|
||||
current_mtime = os.path.getmtime(path)
|
||||
|
||||
# Check cache for valid entry
|
||||
if path_str in self._encoding_cache:
|
||||
cached_encoding, cached_mtime = self._encoding_cache[path_str]
|
||||
if cached_mtime == current_mtime:
|
||||
return cached_encoding
|
||||
|
||||
# No valid cache entry, detect encoding
|
||||
encoding = self.detect_encoding(path)
|
||||
|
||||
# Cache the result with current modification time
|
||||
self._encoding_cache[path_str] = (encoding, current_mtime)
|
||||
return encoding
|
||||
|
||||
|
||||
def with_encoding(method):
|
||||
"""Decorator to handle file encoding for file operations.
|
||||
This decorator automatically detects and applies the correct encoding
|
||||
for file operations, ensuring consistency between read and write operations.
|
||||
Args:
|
||||
method: The method to decorate
|
||||
Returns:
|
||||
The decorated method
|
||||
"""
|
||||
|
||||
@functools.wraps(method)
|
||||
def wrapper(self, path: Path, *args, **kwargs):
|
||||
# Skip encoding handling for directories
|
||||
if path.is_dir():
|
||||
return method(self, path, *args, **kwargs)
|
||||
|
||||
# For files that don't exist yet (like in 'create' command),
|
||||
# use the default encoding
|
||||
if not path.exists():
|
||||
if 'encoding' not in kwargs:
|
||||
kwargs['encoding'] = self._encoding_manager.default_encoding
|
||||
else:
|
||||
# Get encoding from the encoding manager for existing files
|
||||
encoding = self._encoding_manager.get_encoding(path)
|
||||
# Add encoding to kwargs if the method accepts it
|
||||
if 'encoding' not in kwargs:
|
||||
kwargs['encoding'] = encoding
|
||||
|
||||
return method(self, path, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,41 @@
|
||||
class ToolError(Exception):
|
||||
"""Raised when a tool encounters an error."""
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class EditorToolParameterMissingError(ToolError):
|
||||
"""Raised when a required parameter is missing for a tool command."""
|
||||
|
||||
def __init__(self, command, parameter):
|
||||
self.command = command
|
||||
self.parameter = parameter
|
||||
self.message = f'Parameter `{parameter}` is required for command: {command}.'
|
||||
|
||||
|
||||
class EditorToolParameterInvalidError(ToolError):
|
||||
"""Raised when a parameter is invalid for a tool command."""
|
||||
|
||||
def __init__(self, parameter, value, hint=None):
|
||||
self.parameter = parameter
|
||||
self.value = value
|
||||
self.message = (
|
||||
f'Invalid `{parameter}` parameter: {value}. {hint}'
|
||||
if hint
|
||||
else f'Invalid `{parameter}` parameter: {value}.'
|
||||
)
|
||||
|
||||
|
||||
class FileValidationError(ToolError):
|
||||
"""Raised when a file fails validation checks (size, type, etc.)."""
|
||||
|
||||
def __init__(self, path: str, reason: str):
|
||||
self.path = path
|
||||
self.reason = reason
|
||||
self.message = f'File validation failed for {path}: {reason}'
|
||||
super().__init__(self.message)
|
||||
@@ -0,0 +1,146 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileCache:
|
||||
def __init__(self, directory: str, size_limit: Optional[int] = None):
|
||||
self.directory = Path(directory)
|
||||
self.directory.mkdir(parents=True, exist_ok=True)
|
||||
self.size_limit = size_limit
|
||||
self.current_size = 0
|
||||
self._update_current_size()
|
||||
logger.debug(
|
||||
f'FileCache initialized with directory: {self.directory}, size_limit: {self.size_limit}, current_size: {self.current_size}'
|
||||
)
|
||||
|
||||
def _get_file_path(self, key: str) -> Path:
|
||||
hashed_key = hashlib.sha256(key.encode()).hexdigest()
|
||||
return self.directory / f'{hashed_key}.json'
|
||||
|
||||
def _update_current_size(self):
|
||||
self.current_size = sum(
|
||||
f.stat().st_size for f in self.directory.glob('*.json') if f.is_file()
|
||||
)
|
||||
logger.debug(f'Current size updated: {self.current_size}')
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
file_path = self._get_file_path(key)
|
||||
content = json.dumps({'key': key, 'value': value})
|
||||
content_size = len(content.encode('utf-8'))
|
||||
logger.debug(f'Setting key: {key}, content_size: {content_size}')
|
||||
|
||||
if self.size_limit is not None:
|
||||
if file_path.exists():
|
||||
old_size = file_path.stat().st_size
|
||||
size_diff = content_size - old_size
|
||||
logger.debug(
|
||||
f'Existing file: old_size: {old_size}, size_diff: {size_diff}'
|
||||
)
|
||||
if size_diff > 0:
|
||||
while (
|
||||
self.current_size + size_diff > self.size_limit
|
||||
and len(self) > 1
|
||||
):
|
||||
logger.debug(
|
||||
f'Evicting oldest (existing file case): current_size: {self.current_size}, size_limit: {self.size_limit}'
|
||||
)
|
||||
self._evict_oldest(file_path)
|
||||
else:
|
||||
while (
|
||||
self.current_size + content_size > self.size_limit and len(self) > 1
|
||||
):
|
||||
logger.debug(
|
||||
f'Evicting oldest (new file case): current_size: {self.current_size}, size_limit: {self.size_limit}'
|
||||
)
|
||||
self._evict_oldest(file_path)
|
||||
|
||||
if file_path.exists():
|
||||
self.current_size -= file_path.stat().st_size
|
||||
logger.debug(
|
||||
f'Existing file removed from current_size: {self.current_size}'
|
||||
)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
self.current_size += content_size
|
||||
logger.debug(f'File written, new current_size: {self.current_size}')
|
||||
os.utime(
|
||||
file_path, (time.time(), time.time())
|
||||
) # Update access and modification time
|
||||
|
||||
def _evict_oldest(self, exclude_path: Optional[Path] = None):
|
||||
oldest_file = min(
|
||||
(
|
||||
f
|
||||
for f in self.directory.glob('*.json')
|
||||
if f.is_file() and f != exclude_path
|
||||
),
|
||||
key=os.path.getmtime,
|
||||
)
|
||||
evicted_size = oldest_file.stat().st_size
|
||||
self.current_size -= evicted_size
|
||||
os.remove(oldest_file)
|
||||
logger.debug(
|
||||
f'Evicted file: {oldest_file}, size: {evicted_size}, new current_size: {self.current_size}'
|
||||
)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
file_path = self._get_file_path(key)
|
||||
if not file_path.exists():
|
||||
logger.debug(f'Get: Key not found: {key}')
|
||||
return default
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
os.utime(file_path, (time.time(), time.time())) # Update access time
|
||||
logger.debug(f'Get: Key found: {key}')
|
||||
return data['value']
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
file_path = self._get_file_path(key)
|
||||
if file_path.exists():
|
||||
deleted_size = file_path.stat().st_size
|
||||
self.current_size -= deleted_size
|
||||
os.remove(file_path)
|
||||
logger.debug(
|
||||
f'Deleted key: {key}, size: {deleted_size}, new current_size: {self.current_size}'
|
||||
)
|
||||
|
||||
def clear(self) -> None:
|
||||
for item in self.directory.glob('*.json'):
|
||||
if item.is_file():
|
||||
os.remove(item)
|
||||
self.current_size = 0
|
||||
logger.debug('Cache cleared')
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
exists = self._get_file_path(key).exists()
|
||||
logger.debug(f'Contains check: {key}, result: {exists}')
|
||||
return exists
|
||||
|
||||
def __len__(self) -> int:
|
||||
length = sum(1 for _ in self.directory.glob('*.json') if _.is_file())
|
||||
logger.debug(f'Cache length: {length}')
|
||||
return length
|
||||
|
||||
def __iter__(self):
|
||||
for file in self.directory.glob('*.json'):
|
||||
if file.is_file():
|
||||
with open(file, 'r') as f:
|
||||
data = json.load(f)
|
||||
logger.debug(f"Yielding key: {data['key']}")
|
||||
yield data['key']
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.get(key)
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
self.set(key, value)
|
||||
@@ -0,0 +1,119 @@
|
||||
"""History management for file edits with disk-based storage and memory constraints."""
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from .file_cache import FileCache
|
||||
|
||||
|
||||
class FileHistoryManager:
|
||||
"""Manages file edit history with disk-based storage and memory constraints."""
|
||||
|
||||
def __init__(
|
||||
self, max_history_per_file: int = 5, history_dir: Optional[Path] = None
|
||||
):
|
||||
"""Initialize the history manager.
|
||||
|
||||
Args:
|
||||
max_history_per_file: Maximum number of history entries to keep per file (default: 5)
|
||||
history_dir: Directory to store history files. If None, uses a temp directory
|
||||
|
||||
Notes:
|
||||
- Each file's history is limited to the last N entries to conserve memory
|
||||
- The file cache is limited to prevent excessive disk usage
|
||||
- Older entries are automatically removed when limits are exceeded
|
||||
"""
|
||||
self.max_history_per_file = max_history_per_file
|
||||
if history_dir is None:
|
||||
history_dir = Path(tempfile.mkdtemp(prefix='oh_editor_history_'))
|
||||
self.cache = FileCache(str(history_dir))
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _get_metadata_key(self, file_path: Path) -> str:
|
||||
return f'{file_path}.metadata'
|
||||
|
||||
def _get_history_key(self, file_path: Path, counter: int) -> str:
|
||||
return f'{file_path}.{counter}'
|
||||
|
||||
def add_history(self, file_path: Path, content: str):
|
||||
"""Add a new history entry for a file."""
|
||||
metadata_key = self._get_metadata_key(file_path)
|
||||
metadata = self.cache.get(metadata_key, {'entries': [], 'counter': 0})
|
||||
counter = metadata['counter']
|
||||
|
||||
# Add new entry
|
||||
history_key = self._get_history_key(file_path, counter)
|
||||
self.cache.set(history_key, content)
|
||||
|
||||
metadata['entries'].append(counter)
|
||||
metadata['counter'] += 1
|
||||
|
||||
# Keep only last N entries
|
||||
while len(metadata['entries']) > self.max_history_per_file:
|
||||
old_counter = metadata['entries'].pop(0)
|
||||
old_history_key = self._get_history_key(file_path, old_counter)
|
||||
self.cache.delete(old_history_key)
|
||||
|
||||
self.cache.set(metadata_key, metadata)
|
||||
|
||||
def pop_last_history(self, file_path: Path) -> Optional[str]:
|
||||
"""Pop and return the most recent history entry for a file."""
|
||||
metadata_key = self._get_metadata_key(file_path)
|
||||
metadata = self.cache.get(metadata_key, {'entries': [], 'counter': 0})
|
||||
entries = metadata['entries']
|
||||
|
||||
if not entries:
|
||||
return None
|
||||
|
||||
# Pop and remove the last entry
|
||||
last_counter = entries.pop()
|
||||
history_key = self._get_history_key(file_path, last_counter)
|
||||
content = self.cache.get(history_key)
|
||||
|
||||
if content is None:
|
||||
self.logger.warning(f'History entry not found for {file_path}')
|
||||
else:
|
||||
# Remove the entry from the cache
|
||||
self.cache.delete(history_key)
|
||||
|
||||
# Update metadata
|
||||
metadata['entries'] = entries
|
||||
self.cache.set(metadata_key, metadata)
|
||||
|
||||
return content
|
||||
|
||||
def get_metadata(self, file_path: Path):
|
||||
"""Get metadata for a file (for testing purposes)."""
|
||||
metadata_key = self._get_metadata_key(file_path)
|
||||
metadata = self.cache.get(metadata_key, {'entries': [], 'counter': 0})
|
||||
return metadata # Return the actual metadata, not a copy
|
||||
|
||||
def clear_history(self, file_path: Path):
|
||||
"""Clear history for a given file."""
|
||||
metadata_key = self._get_metadata_key(file_path)
|
||||
metadata = self.cache.get(metadata_key, {'entries': [], 'counter': 0})
|
||||
|
||||
# Delete all history entries
|
||||
for counter in metadata['entries']:
|
||||
history_key = self._get_history_key(file_path, counter)
|
||||
self.cache.delete(history_key)
|
||||
|
||||
# Clear metadata
|
||||
self.cache.set(metadata_key, {'entries': [], 'counter': 0})
|
||||
|
||||
def get_all_history(self, file_path: Path) -> List[str]:
|
||||
"""Get all history entries for a file."""
|
||||
metadata_key = self._get_metadata_key(file_path)
|
||||
metadata = self.cache.get(metadata_key, {'entries': [], 'counter': 0})
|
||||
entries = metadata['entries']
|
||||
|
||||
history = []
|
||||
for counter in entries:
|
||||
history_key = self._get_history_key(file_path, counter)
|
||||
content = self.cache.get(history_key)
|
||||
if content is not None:
|
||||
history.append(content)
|
||||
|
||||
return history
|
||||
@@ -0,0 +1,5 @@
|
||||
CONTENT_TRUNCATED_NOTICE = '<response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>'
|
||||
|
||||
FILE_CONTENT_TRUNCATED_NOTICE: str = '<response clipped><NOTE>Due to the max output limit, only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>'
|
||||
|
||||
DIRECTORY_CONTENT_TRUNCATED_NOTICE: str = '<response clipped><NOTE>Due to the max output limit, only part of this directory has been shown to you. You should use `ls -la` instead to view large directories incrementally.</NOTE>'
|
||||
@@ -0,0 +1,49 @@
|
||||
from dataclasses import asdict, dataclass, fields
|
||||
|
||||
from .config import MAX_RESPONSE_LEN_CHAR
|
||||
from .prompts import CONTENT_TRUNCATED_NOTICE
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolResult:
|
||||
"""Represents the result of a tool execution."""
|
||||
|
||||
output: str | None = None
|
||||
error: str | None = None
|
||||
|
||||
def __bool__(self):
|
||||
return any(getattr(self, field.name) for field in fields(self))
|
||||
|
||||
def to_dict(self, extra_field: dict | None = None) -> dict:
|
||||
result = asdict(self)
|
||||
|
||||
# Add extra fields if provided
|
||||
if extra_field:
|
||||
result.update(extra_field)
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class CLIResult(ToolResult):
|
||||
"""A ToolResult that can be rendered as a CLI output."""
|
||||
|
||||
# Optional fields for file editing commands
|
||||
path: str | None = None
|
||||
prev_exist: bool = True
|
||||
old_content: str | None = None
|
||||
new_content: str | None = None
|
||||
|
||||
|
||||
def maybe_truncate(
|
||||
content: str,
|
||||
truncate_after: int | None = MAX_RESPONSE_LEN_CHAR,
|
||||
truncate_notice: str = CONTENT_TRUNCATED_NOTICE,
|
||||
) -> str:
|
||||
"""
|
||||
Truncate content and append a notice if content exceeds the specified length.
|
||||
"""
|
||||
return (
|
||||
content
|
||||
if not truncate_after or len(content) <= truncate_after
|
||||
else content[:truncate_after] + truncate_notice
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Linter module for Dev ACI.
|
||||
|
||||
Part of this Linter module is adapted from Aider (Apache 2.0 License, [original code](https://github.com/paul-gauthier/aider/blob/main/aider/linter.py)). Please see the [original repository](https://github.com/paul-gauthier/aider) for more information.
|
||||
"""
|
||||
|
||||
from .base import LintResult
|
||||
from .linter import DefaultLinter
|
||||
|
||||
__all__ = ['DefaultLinter', 'LintResult']
|
||||
@@ -0,0 +1,79 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LintResult(BaseModel):
|
||||
file: str
|
||||
line: int # 1-indexed
|
||||
column: int # 1-indexed
|
||||
message: str
|
||||
|
||||
def visualize(self, half_window: int = 3) -> str:
|
||||
"""Visualize the lint result by print out all the lines where the lint result is found.
|
||||
|
||||
Args:
|
||||
half_window: The number of context lines to display around the error on each side.
|
||||
"""
|
||||
with open(self.file, 'r') as f:
|
||||
file_lines = f.readlines()
|
||||
|
||||
# Add line numbers
|
||||
_span_size = len(str(len(file_lines)))
|
||||
file_lines = [
|
||||
f'{i + 1:>{_span_size}}|{line.rstrip()}'
|
||||
for i, line in enumerate(file_lines)
|
||||
]
|
||||
|
||||
# Get the window of lines to display
|
||||
assert self.line <= len(file_lines) and self.line > 0
|
||||
line_idx = self.line - 1
|
||||
begin_window = max(0, line_idx - half_window)
|
||||
end_window = min(len(file_lines), line_idx + half_window + 1)
|
||||
|
||||
selected_lines = file_lines[begin_window:end_window]
|
||||
line_idx_in_window = line_idx - begin_window
|
||||
|
||||
# Add character hint
|
||||
_character_hint = (
|
||||
_span_size * ' '
|
||||
+ ' ' * (self.column)
|
||||
+ '^'
|
||||
+ ' ERROR HERE: '
|
||||
+ self.message
|
||||
)
|
||||
selected_lines[line_idx_in_window] = (
|
||||
f'\033[91m{selected_lines[line_idx_in_window]}\033[0m'
|
||||
+ '\n'
|
||||
+ _character_hint
|
||||
)
|
||||
return '\n'.join(selected_lines)
|
||||
|
||||
|
||||
class LinterException(Exception):
|
||||
"""Base class for all linter exceptions."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BaseLinter(ABC):
|
||||
"""Base class for all linters.
|
||||
|
||||
Each linter should be able to lint files of a specific type and return a list of (parsed) lint results.
|
||||
"""
|
||||
|
||||
encoding: str = 'utf-8'
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def supported_extensions(self) -> list[str]:
|
||||
"""The file extensions that this linter supports, such as .py or .tsx."""
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def lint(self, file_path: str) -> list[LintResult]:
|
||||
"""Lint the given file.
|
||||
|
||||
file_path: The path to the file to lint. Required to be absolute.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,99 @@
|
||||
from typing import List
|
||||
|
||||
from dev_aci.utils.logger import oh_aci_logger as logger
|
||||
from dev_aci.utils.shell import run_shell_cmd
|
||||
|
||||
from ..base import BaseLinter, LintResult
|
||||
|
||||
|
||||
def python_compile_lint(fname: str) -> list[LintResult]:
|
||||
try:
|
||||
with open(fname, 'r') as f:
|
||||
code = f.read()
|
||||
compile(code, fname, 'exec') # USE TRACEBACK BELOW HERE
|
||||
return []
|
||||
except SyntaxError as err:
|
||||
err_lineno = getattr(err, 'end_lineno', err.lineno)
|
||||
err_offset = getattr(err, 'end_offset', err.offset)
|
||||
if err_offset and err_offset < 0:
|
||||
err_offset = err.offset
|
||||
return [
|
||||
LintResult(
|
||||
file=fname, line=err_lineno, column=err_offset or 1, message=err.msg
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def flake_lint(filepath: str) -> list[LintResult]:
|
||||
fatal = 'F821,F822,F831,E112,E113,E999,E902'
|
||||
flake8_cmd = f'flake8 --select={fatal} --isolated {filepath}'
|
||||
|
||||
try:
|
||||
cmd_outputs = run_shell_cmd(flake8_cmd, truncate_after=None)[1]
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
results: list[LintResult] = []
|
||||
if not cmd_outputs:
|
||||
return results
|
||||
for line in cmd_outputs.splitlines():
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 4:
|
||||
_msg = parts[3].strip()
|
||||
if len(parts) > 4:
|
||||
_msg += ': ' + parts[4].strip()
|
||||
|
||||
try:
|
||||
line_num = int(parts[1])
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
f'Error parsing flake8 output for line: {e}. Parsed parts: {parts}. Skipping...'
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
column_num = int(parts[2])
|
||||
except ValueError as e:
|
||||
column_num = 1
|
||||
_msg = (
|
||||
parts[2].strip() + ' ' + _msg
|
||||
) # add the unparsed message to the original message
|
||||
logger.warning(
|
||||
f'Error parsing flake8 output for column: {e}. Parsed parts: {parts}. Using default column 1.'
|
||||
)
|
||||
|
||||
results.append(
|
||||
LintResult(
|
||||
file=filepath,
|
||||
line=line_num,
|
||||
column=column_num,
|
||||
message=_msg,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
class PythonLinter(BaseLinter):
|
||||
@property
|
||||
def supported_extensions(self) -> List[str]:
|
||||
return ['.py']
|
||||
|
||||
def lint(self, file_path: str) -> list[LintResult]:
|
||||
error = flake_lint(file_path)
|
||||
if not error:
|
||||
error = python_compile_lint(file_path)
|
||||
return error
|
||||
|
||||
def compile_lint(self, file_path: str, code: str) -> List[LintResult]:
|
||||
try:
|
||||
compile(code, file_path, 'exec')
|
||||
return []
|
||||
except SyntaxError as e:
|
||||
return [
|
||||
LintResult(
|
||||
file=file_path,
|
||||
line=e.lineno,
|
||||
column=e.offset,
|
||||
message=str(e),
|
||||
rule='SyntaxError',
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
import warnings
|
||||
|
||||
from grep_ast import TreeContext, filename_to_lang
|
||||
from grep_ast.parsers import PARSERS
|
||||
|
||||
from ..base import BaseLinter, LintResult
|
||||
from .treesitter_compat import get_parser
|
||||
|
||||
# tree_sitter is throwing a FutureWarning
|
||||
warnings.simplefilter('ignore', category=FutureWarning)
|
||||
|
||||
|
||||
def tree_context(fname, code, line_nums):
|
||||
context = TreeContext(
|
||||
fname,
|
||||
code,
|
||||
color=False,
|
||||
line_number=True,
|
||||
child_context=False,
|
||||
last_line=False,
|
||||
margin=0,
|
||||
mark_lois=True,
|
||||
loi_pad=3,
|
||||
# header_max=30,
|
||||
show_top_of_file_parent_scope=False,
|
||||
)
|
||||
line_nums = set(line_nums)
|
||||
context.add_lines_of_interest(line_nums)
|
||||
context.add_context()
|
||||
output = context.format()
|
||||
return output
|
||||
|
||||
|
||||
def traverse_tree(node):
|
||||
"""Traverses the tree to find errors."""
|
||||
errors = []
|
||||
if node.type == 'ERROR' or node.is_missing:
|
||||
line_no = node.start_point[0] + 1
|
||||
col_no = node.start_point[1] + 1
|
||||
error_type = 'Missing node' if node.is_missing else 'Syntax error'
|
||||
errors.append((line_no, col_no, error_type))
|
||||
|
||||
for child in node.children:
|
||||
errors += traverse_tree(child)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
class TreesitterBasicLinter(BaseLinter):
|
||||
@property
|
||||
def supported_extensions(self) -> list[str]:
|
||||
return list(PARSERS.keys())
|
||||
|
||||
def lint(self, file_path: str) -> list[LintResult]:
|
||||
"""Use tree-sitter to look for syntax errors, display them with tree context."""
|
||||
lang = filename_to_lang(file_path)
|
||||
if not lang:
|
||||
return []
|
||||
parser = get_parser(lang)
|
||||
with open(file_path, 'r') as f:
|
||||
code = f.read()
|
||||
tree = parser.parse(bytes(code, 'utf-8'))
|
||||
errors = traverse_tree(tree.root_node)
|
||||
if not errors:
|
||||
return []
|
||||
return [
|
||||
LintResult(
|
||||
file=file_path,
|
||||
line=int(line),
|
||||
column=int(col),
|
||||
message=error_details,
|
||||
)
|
||||
for line, col, error_details in errors
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Compatibility layer for tree-sitter 0.24.0."""
|
||||
|
||||
import importlib
|
||||
|
||||
from tree_sitter import Language, Parser
|
||||
|
||||
# Cache of loaded languages
|
||||
_language_cache = {}
|
||||
|
||||
|
||||
def get_parser(language):
|
||||
"""Get a Parser object for the given language name."""
|
||||
if language not in _language_cache:
|
||||
# Try to import the language module
|
||||
module_name = f'tree_sitter_{language}'
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
_language_cache[language] = Language(module.language())
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
f'Language {language} is not supported. Please install {module_name} package.'
|
||||
)
|
||||
|
||||
return Parser(_language_cache[language])
|
||||
@@ -0,0 +1,122 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
from ..linter.base import BaseLinter, LinterException, LintResult
|
||||
from ..linter.impl.python import PythonLinter
|
||||
from ..linter.impl.treesitter import TreesitterBasicLinter
|
||||
|
||||
|
||||
class DefaultLinter(BaseLinter):
|
||||
def __init__(self):
|
||||
self.linters: dict[str, list[BaseLinter]] = defaultdict(list)
|
||||
self.linters['.py'] = [PythonLinter()]
|
||||
|
||||
# Add treesitter linter as a fallback for all linters
|
||||
self.basic_linter = TreesitterBasicLinter()
|
||||
for extension in self.basic_linter.supported_extensions:
|
||||
self.linters[extension].append(self.basic_linter)
|
||||
self._supported_extensions = list(self.linters.keys())
|
||||
|
||||
@property
|
||||
def supported_extensions(self) -> list[str]:
|
||||
return self._supported_extensions
|
||||
|
||||
def lint(self, file_path: str) -> list[LintResult]:
|
||||
if not os.path.isabs(file_path):
|
||||
raise LinterException(f'File path {file_path} is not an absolute path')
|
||||
file_extension = os.path.splitext(file_path)[1]
|
||||
|
||||
linters: list[BaseLinter] = self.linters.get(file_extension, [])
|
||||
for linter in linters:
|
||||
res = linter.lint(file_path)
|
||||
# We always return the first linter's result (higher priority)
|
||||
if res:
|
||||
return res
|
||||
return []
|
||||
|
||||
def lint_file_diff(
|
||||
self, original_file_path: str, updated_file_path: str
|
||||
) -> list[LintResult]:
|
||||
"""Only return lint errors that are introduced by the diff.
|
||||
|
||||
Args:
|
||||
original_file_path: The original file path.
|
||||
updated_file_path: The updated file path.
|
||||
|
||||
Returns:
|
||||
A list of lint errors that are introduced by the diff.
|
||||
"""
|
||||
# 1. Lint the original and updated file
|
||||
original_lint_errors: list[LintResult] = self.lint(original_file_path)
|
||||
updated_lint_errors: list[LintResult] = self.lint(updated_file_path)
|
||||
|
||||
# 2. Load the original and updated file content
|
||||
with open(original_file_path, 'r') as f:
|
||||
old_lines = f.readlines()
|
||||
with open(updated_file_path, 'r') as f:
|
||||
new_lines = f.readlines()
|
||||
|
||||
# 3. Get line numbers that are changed & unchanged
|
||||
# Map the line number of the original file to the updated file
|
||||
# NOTE: this only works for lines that are not changed (i.e., equal)
|
||||
old_to_new_line_no_mapping: dict[int, int] = {}
|
||||
replace_or_inserted_lines: list[int] = []
|
||||
for (
|
||||
tag,
|
||||
old_idx_start,
|
||||
old_idx_end,
|
||||
new_idx_start,
|
||||
new_idx_end,
|
||||
) in SequenceMatcher(
|
||||
isjunk=None,
|
||||
a=old_lines,
|
||||
b=new_lines,
|
||||
).get_opcodes():
|
||||
if tag == 'equal':
|
||||
for idx, _ in enumerate(old_lines[old_idx_start:old_idx_end]):
|
||||
old_to_new_line_no_mapping[old_idx_start + idx + 1] = (
|
||||
new_idx_start + idx + 1
|
||||
)
|
||||
elif tag == 'replace' or tag == 'insert':
|
||||
for idx, _ in enumerate(old_lines[old_idx_start:old_idx_end]):
|
||||
replace_or_inserted_lines.append(new_idx_start + idx + 1)
|
||||
else:
|
||||
# omit the case of delete
|
||||
pass
|
||||
|
||||
# 4. Get pre-existing errors in unchanged lines
|
||||
# increased error elsewhere introduced by the newlines
|
||||
# i.e., we omit errors that are already in original files and report new one
|
||||
new_line_no_to_original_errors: dict[int, list[LintResult]] = defaultdict(list)
|
||||
for error in original_lint_errors:
|
||||
if error.line in old_to_new_line_no_mapping:
|
||||
new_line_no_to_original_errors[
|
||||
old_to_new_line_no_mapping[error.line]
|
||||
].append(error)
|
||||
|
||||
# 5. Select errors from lint results in new file to report
|
||||
selected_errors = []
|
||||
for error in updated_lint_errors:
|
||||
# 5.1. Error introduced by replace/insert
|
||||
if error.line in replace_or_inserted_lines:
|
||||
selected_errors.append(error)
|
||||
# 5.2. Error introduced by modified lines that impacted
|
||||
# the unchanged lines that HAVE pre-existing errors
|
||||
elif error.line in new_line_no_to_original_errors:
|
||||
# skip if the error is already reported
|
||||
# or add if the error is new
|
||||
if not any(
|
||||
original_error.message == error.message
|
||||
and original_error.column == error.column
|
||||
for original_error in new_line_no_to_original_errors[error.line]
|
||||
):
|
||||
selected_errors.append(error)
|
||||
# 5.3. Error introduced by modified lines that impacted
|
||||
# the unchanged lines that have NO pre-existing errors
|
||||
else:
|
||||
selected_errors.append(error)
|
||||
|
||||
# 6. Sort errors by line and column
|
||||
selected_errors.sort(key=lambda x: (x.line, x.column))
|
||||
return selected_errors
|
||||
@@ -0,0 +1,41 @@
|
||||
import difflib
|
||||
|
||||
import whatthepatch
|
||||
|
||||
|
||||
def get_diff(old_contents: str, new_contents: str, filepath: str = 'file') -> str:
|
||||
diff = list(
|
||||
difflib.unified_diff(
|
||||
old_contents.split('\n'),
|
||||
new_contents.split('\n'),
|
||||
fromfile=filepath,
|
||||
tofile=filepath,
|
||||
# do not output unchange lines
|
||||
# because they can cause `parse_diff` to fail
|
||||
n=0,
|
||||
)
|
||||
)
|
||||
return '\n'.join(map(lambda x: x.rstrip(), diff))
|
||||
|
||||
|
||||
def parse_diff(diff_patch: str) -> list[whatthepatch.patch.Change]:
|
||||
# handle empty patch
|
||||
if diff_patch.strip() == '':
|
||||
return []
|
||||
|
||||
patch = whatthepatch.parse_patch(diff_patch)
|
||||
patch_list = list(patch)
|
||||
assert len(patch_list) == 1, (
|
||||
'parse_diff only supports single file diff. But got:\nPATCH:\n'
|
||||
+ diff_patch
|
||||
+ '\nPATCH LIST:\n'
|
||||
+ str(patch_list)
|
||||
)
|
||||
changes = patch_list[0].changes
|
||||
|
||||
# ignore changes that are the same (i.e., old_lineno == new_lineno)
|
||||
output_changes = []
|
||||
for change in changes:
|
||||
if change.old != change.new:
|
||||
output_changes.append(change)
|
||||
return output_changes
|
||||
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||
|
||||
DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
|
||||
if DEBUG:
|
||||
LOG_LEVEL = 'DEBUG'
|
||||
|
||||
oh_aci_logger = logging.getLogger('dev_aci')
|
||||
|
||||
current_log_level = logging.INFO
|
||||
if LOG_LEVEL in logging.getLevelNamesMapping():
|
||||
current_log_level = logging.getLevelNamesMapping()[LOG_LEVEL]
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(current_log_level)
|
||||
formatter = logging.Formatter(
|
||||
'{asctime} - {name}:{levelname} - {message}',
|
||||
style='{',
|
||||
datefmt='%Y-%m-%d %H:%M',
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
oh_aci_logger.setLevel(current_log_level)
|
||||
oh_aci_logger.addHandler(console_handler)
|
||||
oh_aci_logger.propagate = False
|
||||
oh_aci_logger.debug('Logger initialized')
|
||||
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from dev_aci.editor.config import MAX_RESPONSE_LEN_CHAR
|
||||
from dev_aci.editor.prompts import CONTENT_TRUNCATED_NOTICE
|
||||
from dev_aci.editor.results import maybe_truncate
|
||||
|
||||
|
||||
def run_shell_cmd(
|
||||
cmd: str,
|
||||
timeout: float | None = 120.0, # seconds
|
||||
truncate_after: int | None = MAX_RESPONSE_LEN_CHAR,
|
||||
truncate_notice: str = CONTENT_TRUNCATED_NOTICE,
|
||||
) -> tuple[int, str, str]:
|
||||
"""Run a shell command synchronously with a timeout.
|
||||
|
||||
Args:
|
||||
cmd: The shell command to run.
|
||||
timeout: The maximum time to wait for the command to complete.
|
||||
truncate_after: The maximum number of characters to return for stdout and stderr.
|
||||
|
||||
Returns:
|
||||
A tuple containing the return code, stdout, and stderr.
|
||||
"""
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
stdout, stderr = process.communicate(timeout=timeout)
|
||||
|
||||
return (
|
||||
process.returncode or 0,
|
||||
maybe_truncate(
|
||||
stdout, truncate_after=truncate_after, truncate_notice=truncate_notice
|
||||
),
|
||||
maybe_truncate(
|
||||
stderr,
|
||||
truncate_after=truncate_after,
|
||||
truncate_notice=CONTENT_TRUNCATED_NOTICE,
|
||||
), # Use generic notice for stderr
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
elapsed_time = time.time() - start_time
|
||||
raise TimeoutError(
|
||||
f"Command '{cmd}' timed out after {elapsed_time:.2f} seconds"
|
||||
)
|
||||
|
||||
|
||||
def check_tool_installed(tool_name: str) -> bool:
|
||||
"""Check if a tool is installed."""
|
||||
try:
|
||||
subprocess.run(
|
||||
[tool_name, '--version'],
|
||||
check=True,
|
||||
cwd=os.getcwd(),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
@@ -0,0 +1,43 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude: docs/modules/python
|
||||
- id: end-of-file-fixer
|
||||
exclude: docs/modules/python
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 1.7.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.16
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
entry: ruff check --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
entry: ruff format --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
[types-requests, types-setuptools, types-pyyaml, types-toml, types-cachetools]
|
||||
entry: mypy --config-file dev_config/python/mypy.ini dev_aci/
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
@@ -0,0 +1,9 @@
|
||||
[mypy]
|
||||
warn_unused_configs = True
|
||||
ignore_missing_imports = True
|
||||
check_untyped_defs = True
|
||||
explicit_package_bases = True
|
||||
warn_unreachable = True
|
||||
warn_redundant_casts = True
|
||||
no_implicit_optional = True
|
||||
strict_optional = True
|
||||
@@ -0,0 +1,26 @@
|
||||
[lint]
|
||||
select = [
|
||||
"E",
|
||||
"W",
|
||||
"F",
|
||||
"I",
|
||||
"Q",
|
||||
"B",
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"E501",
|
||||
"B003",
|
||||
"B007",
|
||||
"B009",
|
||||
"B010",
|
||||
"B904",
|
||||
"B018",
|
||||
]
|
||||
|
||||
[lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "single"
|
||||
|
||||
[format]
|
||||
quote-style = "single"
|
||||
@@ -0,0 +1,101 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: hanzo-aci
|
||||
Version: 0.2.8
|
||||
Summary: An Agent-Computer Interface (ACI) designed for software development agents Dev.
|
||||
Author: Hanzo Industries Inc
|
||||
License: MIT
|
||||
Requires-Python: >=3.12
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: numpy
|
||||
Requires-Dist: pandas
|
||||
Requires-Dist: scipy
|
||||
Requires-Dist: networkx
|
||||
Requires-Dist: litellm
|
||||
Requires-Dist: gitpython
|
||||
Requires-Dist: tree-sitter>=0.24.0
|
||||
Requires-Dist: tree-sitter-python>=0.23.6
|
||||
Requires-Dist: tree-sitter-javascript>=0.23.1
|
||||
Requires-Dist: tree-sitter-typescript>=0.23.2
|
||||
Requires-Dist: tree-sitter-ruby>=0.23.1
|
||||
Requires-Dist: grep-ast>=0.8.1
|
||||
Requires-Dist: flake8
|
||||
Requires-Dist: whatthepatch>=1.0.6
|
||||
Requires-Dist: binaryornot>=0.4.4
|
||||
Requires-Dist: cachetools>=5.5.2
|
||||
Requires-Dist: charset-normalizer>=3.4.1
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: ruff>=0.7.2; extra == "dev"
|
||||
Requires-Dist: pre-commit>=4.0.1; extra == "dev"
|
||||
Provides-Extra: test
|
||||
Requires-Dist: pytest>=8.3.3; extra == "test"
|
||||
Requires-Dist: psutil>=5.9.8; extra == "test"
|
||||
Dynamic: license-file
|
||||
|
||||
# Agent-Computer Interface (ACI) for Dev
|
||||
|
||||
An Agent-Computer Interface (ACI) designed for software development agents [Dev](https://github.com/hanzoai/Dev). This package provides essential tools and interfaces for AI agents to interact with computer systems for software development tasks.
|
||||
|
||||
## Features
|
||||
|
||||
- **Code Editor Interface**: Sophisticated editing capabilities through the `editor` module
|
||||
- File creation and modification
|
||||
- Code editing
|
||||
- Configuration management
|
||||
|
||||
- **Code Linting**: Built-in linting capabilities via the `linter` module
|
||||
- Tree-sitter based code analysis
|
||||
- Python-specific linting support
|
||||
|
||||
- **Utility Functions**: Helper modules for common operations
|
||||
- Shell command execution utilities
|
||||
- Diff generation and analysis
|
||||
- Logging functionality
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install dev-aci
|
||||
```
|
||||
|
||||
Or using Poetry:
|
||||
|
||||
```bash
|
||||
poetry add dev-aci
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
dev_aci/
|
||||
├── editor/ # Code editing functionality
|
||||
├── linter/ # Code linting capabilities
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/dev-aci.git
|
||||
cd dev-aci
|
||||
```
|
||||
|
||||
2. Install development dependencies:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Configure pre-commit-hooks
|
||||
```bash
|
||||
make install-pre-commit-hooks
|
||||
```
|
||||
|
||||
4. Run tests:
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -0,0 +1,27 @@
|
||||
LICENSE
|
||||
README.md
|
||||
pyproject.toml
|
||||
dev_aci/__init__.py
|
||||
dev_aci/editor/__init__.py
|
||||
dev_aci/editor/config.py
|
||||
dev_aci/editor/editor.py
|
||||
dev_aci/editor/encoding.py
|
||||
dev_aci/editor/exceptions.py
|
||||
dev_aci/editor/file_cache.py
|
||||
dev_aci/editor/history.py
|
||||
dev_aci/editor/prompts.py
|
||||
dev_aci/editor/results.py
|
||||
dev_aci/linter/__init__.py
|
||||
dev_aci/linter/base.py
|
||||
dev_aci/linter/linter.py
|
||||
dev_aci/linter/impl/python.py
|
||||
dev_aci/linter/impl/treesitter.py
|
||||
dev_aci/linter/impl/treesitter_compat.py
|
||||
dev_aci/utils/diff.py
|
||||
dev_aci/utils/logger.py
|
||||
dev_aci/utils/shell.py
|
||||
hanzo_aci.egg-info/PKG-INFO
|
||||
hanzo_aci.egg-info/SOURCES.txt
|
||||
hanzo_aci.egg-info/dependency_links.txt
|
||||
hanzo_aci.egg-info/requires.txt
|
||||
hanzo_aci.egg-info/top_level.txt
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
numpy
|
||||
pandas
|
||||
scipy
|
||||
networkx
|
||||
litellm
|
||||
gitpython
|
||||
tree-sitter>=0.24.0
|
||||
tree-sitter-python>=0.23.6
|
||||
tree-sitter-javascript>=0.23.1
|
||||
tree-sitter-typescript>=0.23.2
|
||||
tree-sitter-ruby>=0.23.1
|
||||
grep-ast>=0.8.1
|
||||
flake8
|
||||
whatthepatch>=1.0.6
|
||||
binaryornot>=0.4.4
|
||||
cachetools>=5.5.2
|
||||
charset-normalizer>=3.4.1
|
||||
|
||||
[dev]
|
||||
ruff>=0.7.2
|
||||
pre-commit>=4.0.1
|
||||
|
||||
[test]
|
||||
pytest>=8.3.3
|
||||
psutil>=5.9.8
|
||||
@@ -0,0 +1 @@
|
||||
dev_aci
|
||||
Generated
+2730
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
[project]
|
||||
name = "hanzo-aci"
|
||||
version = "0.2.8"
|
||||
description = "An Agent-Computer Interface (ACI) designed for software development agents Dev."
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Hanzo Industries Inc" }
|
||||
]
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pandas",
|
||||
"scipy",
|
||||
"networkx",
|
||||
"litellm",
|
||||
"gitpython",
|
||||
"tree-sitter>=0.24.0",
|
||||
"tree-sitter-python>=0.23.6",
|
||||
"tree-sitter-javascript>=0.23.1",
|
||||
"tree-sitter-typescript>=0.23.2",
|
||||
"tree-sitter-ruby>=0.23.1",
|
||||
"grep-ast>=0.8.1",
|
||||
"flake8",
|
||||
"whatthepatch>=1.0.6",
|
||||
"binaryornot>=0.4.4",
|
||||
"cachetools>=5.5.2",
|
||||
"charset-normalizer>=3.4.1"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff>=0.7.2",
|
||||
"pre-commit>=4.0.1"
|
||||
]
|
||||
test = [
|
||||
"pytest>=8.3.3",
|
||||
"psutil>=5.9.8"
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["dev_aci*"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.autopep8]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.black]
|
||||
skip-string-normalization = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["D"]
|
||||
ignore = ["D1"]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
addopts = -p no:warnings --ignore=oh-viewer
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for file editor functionality."""
|
||||
@@ -0,0 +1,21 @@
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
"""Create a temporary file for testing."""
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
yield Path(f.name)
|
||||
try:
|
||||
Path(f.name).unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def parse_result(result: str) -> dict:
|
||||
"""Parse the JSON result from file_editor."""
|
||||
return json.loads(result[result.find('{') : result.rfind('}') + 1])
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Tests for basic file editor operations."""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from dev_aci.editor import file_editor
|
||||
|
||||
from .conftest import parse_result
|
||||
|
||||
|
||||
def test_file_editor_happy_path(temp_file):
|
||||
command = 'str_replace'
|
||||
old_str = 'test file'
|
||||
new_str = 'sample file'
|
||||
|
||||
# Create test file
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write('This is a test file.\nThis file is for testing purposes.')
|
||||
|
||||
# Call the `file_editor` function
|
||||
result = file_editor(
|
||||
command=command,
|
||||
path=temp_file,
|
||||
old_str=old_str,
|
||||
new_str=new_str,
|
||||
enable_linting=False,
|
||||
)
|
||||
|
||||
# Extract the JSON content using a regular expression
|
||||
match = re.search(
|
||||
r'<oh_aci_output_[0-9a-f]{32}>(.*?)</oh_aci_output_[0-9a-f]{32}>',
|
||||
result,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, 'Output does not contain the expected <oh_aci_output_> tags in the correct format.'
|
||||
result_dict = json.loads(match.group(1))
|
||||
|
||||
# Validate the formatted output in the result dictionary
|
||||
formatted_output = result_dict['formatted_output_and_error']
|
||||
assert (
|
||||
formatted_output
|
||||
== f"""The file {temp_file} has been edited. Here's the result of running `cat -n` on a snippet of {temp_file}:
|
||||
1\tThis is a sample file.
|
||||
2\tThis file is for testing purposes.
|
||||
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
||||
)
|
||||
assert result_dict['path'] == str(temp_file)
|
||||
assert result_dict['prev_exist'] is True
|
||||
assert (
|
||||
result_dict['old_content']
|
||||
== 'This is a test file.\nThis file is for testing purposes.'
|
||||
)
|
||||
assert (
|
||||
result_dict['new_content']
|
||||
== 'This is a sample file.\nThis file is for testing purposes.'
|
||||
)
|
||||
|
||||
# Ensure the file content was updated
|
||||
with open(temp_file, 'r') as f:
|
||||
content = f.read()
|
||||
assert 'This is a sample file.' in content
|
||||
|
||||
|
||||
def test_file_editor_with_xml_tag_parsing(temp_file):
|
||||
# Create content that includes the XML tag pattern
|
||||
xml_content = """This is a file with XML tags parsing logic...
|
||||
match = re.search(
|
||||
r'<oh_aci_output_[0-9a-f]{32}>(.*?)</oh_aci_output_[0-9a-f]{32}>',
|
||||
result,
|
||||
re.DOTALL,
|
||||
)
|
||||
...More text here.
|
||||
"""
|
||||
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(xml_content)
|
||||
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
)
|
||||
|
||||
# Ensure the content is extracted correctly
|
||||
match = re.search(
|
||||
r'<oh_aci_output_[0-9a-f]{32}>(.*?)</oh_aci_output_[0-9a-f]{32}>',
|
||||
result,
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
assert match, 'Output does not contain the expected <oh_aci_output_> tags in the correct format.'
|
||||
result_dict = json.loads(match.group(1))
|
||||
|
||||
# Validate the formatted output in the result dictionary
|
||||
formatted_output = result_dict['formatted_output_and_error']
|
||||
assert (
|
||||
formatted_output
|
||||
== f"""Here's the result of running `cat -n` on {temp_file}:
|
||||
1\tThis is a file with XML tags parsing logic...
|
||||
2\tmatch = re.search(
|
||||
3\t r'<oh_aci_output_[0-9a-f]{{32}}>(.*?)</oh_aci_output_[0-9a-f]{{32}}>',
|
||||
4\t result,
|
||||
5\t re.DOTALL,
|
||||
6\t)
|
||||
7\t...More text here.
|
||||
8\t
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_successful_operations(temp_file):
|
||||
"""Test successful file operations and their output formatting."""
|
||||
# Create a test file
|
||||
content = 'line 1\nline 2\nline 3\n'
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Test view
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert (
|
||||
"Here's the result of running `cat -n`"
|
||||
in result_json['formatted_output_and_error']
|
||||
)
|
||||
assert 'line 1' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test str_replace
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=temp_file,
|
||||
old_str='line 2',
|
||||
new_str='replaced line',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'has been edited' in result_json['formatted_output_and_error']
|
||||
assert 'replaced line' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test insert
|
||||
result = file_editor(
|
||||
command='insert',
|
||||
path=temp_file,
|
||||
insert_line=1,
|
||||
new_str='inserted line',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'has been edited' in result_json['formatted_output_and_error']
|
||||
assert 'inserted line' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test undo
|
||||
result = file_editor(
|
||||
command='undo_edit',
|
||||
path=temp_file,
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'undone successfully' in result_json['formatted_output_and_error']
|
||||
|
||||
|
||||
def test_tab_expansion(temp_file):
|
||||
"""Test that tabs are properly expanded in file operations."""
|
||||
# Create a file with tabs
|
||||
content = 'no tabs\n\tindented\nline\twith\ttabs\n'
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Test view command
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
# Tabs should be expanded to spaces in output
|
||||
assert ' indented' in result_json['formatted_output_and_error']
|
||||
assert 'line with tabs' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test str_replace with tabs in old_str
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=temp_file,
|
||||
old_str='line\twith\ttabs',
|
||||
new_str='replaced line',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'replaced line' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test str_replace with tabs in new_str
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=temp_file,
|
||||
old_str='replaced line',
|
||||
new_str='new\tline\twith\ttabs',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
# Tabs should be expanded in the output
|
||||
assert 'new line with tabs' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test insert with tabs
|
||||
result = file_editor(
|
||||
command='insert',
|
||||
path=temp_file,
|
||||
insert_line=1,
|
||||
new_str='\tindented\tline',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
# Tabs should be expanded in the output
|
||||
assert ' indented line' in result_json['formatted_output_and_error']
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Tests for error handling in file editor."""
|
||||
|
||||
from dev_aci.editor import file_editor
|
||||
|
||||
from .conftest import parse_result
|
||||
|
||||
|
||||
def test_validation_error_formatting():
|
||||
"""Test that validation errors are properly formatted in the output."""
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path='/nonexistent/file.txt',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'does not exist' in result_json['formatted_output_and_error']
|
||||
assert (
|
||||
result_json['error']
|
||||
== 'Invalid `path` parameter: /nonexistent/file.txt. The path /nonexistent/file.txt does not exist. Please provide a valid path.'
|
||||
)
|
||||
|
||||
# Test directory validation for non-view commands
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path='/tmp',
|
||||
old_str='something',
|
||||
new_str='new',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'only the `view` command' in result_json['formatted_output_and_error']
|
||||
assert 'directory and only the `view` command' in result_json['error']
|
||||
|
||||
|
||||
def test_str_replace_error_handling(temp_file):
|
||||
"""Test error handling in str_replace command."""
|
||||
# Create a test file
|
||||
content = 'line 1\nline 2\nline 3\n'
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Test non-existent string
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=temp_file,
|
||||
old_str='nonexistent',
|
||||
new_str='something',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'did not appear verbatim' in result_json['formatted_output_and_error']
|
||||
assert 'did not appear verbatim' in result_json['error']
|
||||
|
||||
# Test multiple occurrences
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write('line\nline\nother')
|
||||
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=temp_file,
|
||||
old_str='line',
|
||||
new_str='new_line',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'Multiple occurrences' in result_json['formatted_output_and_error']
|
||||
assert 'lines [1, 2]' in result_json['error']
|
||||
|
||||
|
||||
def test_view_range_validation(temp_file):
|
||||
"""Test validation of view_range parameter."""
|
||||
# Create a test file
|
||||
content = 'line 1\nline 2\nline 3\n'
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Test invalid range format
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
view_range=[1], # Should be [start, end]
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert (
|
||||
'should be a list of two integers' in result_json['formatted_output_and_error']
|
||||
)
|
||||
|
||||
# Test out of bounds range
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
view_range=[1, 10], # File only has 3 lines
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert (
|
||||
'should be smaller than the number of lines'
|
||||
in result_json['formatted_output_and_error']
|
||||
)
|
||||
|
||||
# Test invalid range order
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
view_range=[3, 1], # End before start
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert (
|
||||
'should be greater than or equal to'
|
||||
in result_json['formatted_output_and_error']
|
||||
)
|
||||
|
||||
|
||||
def test_insert_validation(temp_file):
|
||||
"""Test validation in insert command."""
|
||||
# Create a test file
|
||||
content = 'line 1\nline 2\nline 3\n'
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Test insert at negative line
|
||||
result = file_editor(
|
||||
command='insert',
|
||||
path=temp_file,
|
||||
insert_line=-1,
|
||||
new_str='new line',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'should be within the range' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test insert beyond file length
|
||||
result = file_editor(
|
||||
command='insert',
|
||||
path=temp_file,
|
||||
insert_line=10,
|
||||
new_str='new line',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'should be within the range' in result_json['formatted_output_and_error']
|
||||
|
||||
|
||||
def test_undo_validation(temp_file):
|
||||
"""Test undo_edit validation."""
|
||||
# Create a test file
|
||||
content = 'line 1\nline 2\nline 3\n'
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Try to undo without any previous edits
|
||||
result = file_editor(
|
||||
command='undo_edit',
|
||||
path=temp_file,
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'No edit history found' in result_json['formatted_output_and_error']
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Tests for file validation in file editor."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dev_aci.editor import file_editor
|
||||
|
||||
from .conftest import parse_result
|
||||
|
||||
|
||||
def test_file_validation(temp_file):
|
||||
"""Test file validation for various file types."""
|
||||
# Ensure temp_file has .sql suffix
|
||||
temp_file_sql = Path(temp_file).with_suffix('.sql')
|
||||
os.rename(temp_file, temp_file_sql)
|
||||
|
||||
# Test binary file
|
||||
with open(temp_file_sql, 'wb') as f:
|
||||
f.write(b'Some text\x00with binary\x00content')
|
||||
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_file_sql),
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'binary' in result_json['formatted_output_and_error'].lower()
|
||||
|
||||
# Test large file
|
||||
large_size = 11 * 1024 * 1024 # 11MB
|
||||
with open(temp_file_sql, 'w') as f:
|
||||
f.write('x' * large_size)
|
||||
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_file_sql),
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'too large' in result_json['formatted_output_and_error']
|
||||
assert '10MB' in result_json['formatted_output_and_error']
|
||||
|
||||
# Test SQL file
|
||||
sql_content = """
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id = 1;
|
||||
"""
|
||||
with open(temp_file_sql, 'w') as f:
|
||||
f.write(sql_content)
|
||||
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_file_sql),
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'SELECT *' in result_json['formatted_output_and_error']
|
||||
assert 'binary' not in result_json['formatted_output_and_error'].lower()
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Tests for memory usage in file editor."""
|
||||
|
||||
import gc
|
||||
import os
|
||||
|
||||
import psutil
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor import file_editor
|
||||
|
||||
from .conftest import parse_result
|
||||
|
||||
|
||||
def test_file_read_memory_usage(temp_file):
|
||||
"""Test that reading a large file uses memory efficiently."""
|
||||
# Create a large file (9.5MB to stay under 10MB limit)
|
||||
file_size_mb = 9.5
|
||||
line_size = 100 # bytes per line approximately
|
||||
num_lines = int((file_size_mb * 1024 * 1024) // line_size)
|
||||
|
||||
print(f'\nCreating test file with {num_lines} lines...')
|
||||
with open(temp_file, 'w') as f:
|
||||
for i in range(num_lines):
|
||||
f.write(f'Line {i}: ' + 'x' * (line_size - 10) + '\n')
|
||||
|
||||
actual_size = os.path.getsize(temp_file) / (1024 * 1024)
|
||||
print(f'File created, size: {actual_size:.2f} MB')
|
||||
|
||||
# Force Python to release file handles and clear buffers
|
||||
gc.collect()
|
||||
|
||||
# Get initial memory usage
|
||||
initial_memory = psutil.Process(os.getpid()).memory_info().rss
|
||||
print(f'Initial memory usage: {initial_memory / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Test reading specific lines
|
||||
try:
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=temp_file,
|
||||
view_range=[5000, 5100], # Read 100 lines from middle
|
||||
enable_linting=False,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'\nError during file read: {str(e)}')
|
||||
raise
|
||||
|
||||
# Check memory usage after reading
|
||||
current_memory = psutil.Process(os.getpid()).memory_info().rss
|
||||
memory_growth = current_memory - initial_memory
|
||||
print(
|
||||
f'Memory growth after reading 100 lines: {memory_growth / 1024 / 1024:.2f} MB'
|
||||
)
|
||||
|
||||
# Memory growth should be small since we're only reading 100 lines
|
||||
# Allow for some overhead but it should be much less than file size
|
||||
# Increased to 3MB to account for chardet's memory usage
|
||||
max_growth_mb = 3 # 3MB max growth
|
||||
assert memory_growth <= max_growth_mb * 1024 * 1024, (
|
||||
f'Memory growth too high: {memory_growth / 1024 / 1024:.2f} MB '
|
||||
f'(limit: {max_growth_mb} MB)'
|
||||
)
|
||||
|
||||
# Parse the JSON output
|
||||
try:
|
||||
result_json = parse_result(result)
|
||||
content = result_json['formatted_output_and_error']
|
||||
except Exception as e:
|
||||
print(f'\nError parsing result: {str(e)}')
|
||||
print(f'Result: {result[:200]}...')
|
||||
raise
|
||||
|
||||
# Extract the actual content (skip the header)
|
||||
content_start = content.find("Here's the result of running `cat -n`")
|
||||
if content_start == -1:
|
||||
print(f'\nUnexpected content format: {content[:200]}...')
|
||||
raise ValueError('Could not find expected content header')
|
||||
content_start = content.find('\n', content_start) + 1
|
||||
content = content[content_start:]
|
||||
|
||||
# Verify we got the correct lines
|
||||
line_count = content.count('\n')
|
||||
assert line_count >= 99, f'Should have read at least 99 lines, got {line_count}'
|
||||
assert 'Line 5000:' in content, 'Should contain the first requested line'
|
||||
assert 'Line 5099:' in content, 'Should contain the last requested line'
|
||||
|
||||
print('Test completed successfully')
|
||||
|
||||
|
||||
def test_file_editor_memory_leak(temp_file):
|
||||
"""Test to demonstrate memory growth during multiple file edits."""
|
||||
print('\nStarting memory leak test...')
|
||||
|
||||
# Set memory limit to 128MB to make it more likely to catch issues
|
||||
memory_limit = 128 * 1024 * 1024 # 128MB in bytes
|
||||
try:
|
||||
import resource
|
||||
|
||||
resource.setrlimit(resource.RLIMIT_AS, (memory_limit, memory_limit))
|
||||
print('Memory limit set successfully')
|
||||
except Exception as e:
|
||||
print(f'Warning: Could not set memory limit: {str(e)}')
|
||||
|
||||
initial_memory = psutil.Process(os.getpid()).memory_info().rss
|
||||
print(f'\nInitial memory usage: {initial_memory / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Create initial content that's large enough to test but not overwhelming
|
||||
# Keep total file size under 10MB to avoid file validation errors
|
||||
base_content = (
|
||||
'Initial content with some reasonable length to make the file larger\n'
|
||||
)
|
||||
content = base_content * 100
|
||||
print(f'\nCreating initial file with {len(content)} bytes')
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
print(f'Initial file created, size: {os.path.getsize(temp_file) / 1024:.1f} KB')
|
||||
|
||||
try:
|
||||
# Store memory readings for analysis
|
||||
memory_readings = []
|
||||
file_size_mb = 0
|
||||
|
||||
# Perform edits with reasonable content size
|
||||
for i in range(1000): # Increased iterations, smaller content per iteration
|
||||
# Create content for each edit - keep it small to avoid file size limits
|
||||
old_content = f'content_{i}\n' * 5 # 5 lines per edit
|
||||
new_content = f'content_{i + 1}\n' * 5
|
||||
|
||||
# Instead of appending, we'll replace content to keep file size stable
|
||||
with open(temp_file, 'r') as f:
|
||||
current_content = f.read()
|
||||
|
||||
# Insert old_content at a random position while keeping file size stable
|
||||
insert_pos = len(current_content) // 2
|
||||
new_file_content = (
|
||||
current_content[:insert_pos]
|
||||
+ old_content
|
||||
+ current_content[insert_pos + len(old_content) :]
|
||||
)
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(new_file_content)
|
||||
|
||||
# Perform the edit
|
||||
try:
|
||||
if i == 0:
|
||||
print(
|
||||
f'\nInitial file size: {os.path.getsize(temp_file) / (1024 * 1024):.2f} MB'
|
||||
)
|
||||
print(f'Sample content to replace: {old_content[:100]}...')
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=temp_file,
|
||||
old_str=old_content,
|
||||
new_str=new_content,
|
||||
enable_linting=False,
|
||||
)
|
||||
if i == 0:
|
||||
print(f'First edit result: {result[:200]}...')
|
||||
except Exception as e:
|
||||
print(f'\nError during edit {i}:')
|
||||
print(f'File size: {os.path.getsize(temp_file) / (1024 * 1024):.2f} MB')
|
||||
print(f'Error: {str(e)}')
|
||||
raise
|
||||
|
||||
if i % 25 == 0: # Check more frequently
|
||||
current_memory = psutil.Process(os.getpid()).memory_info().rss
|
||||
memory_mb = current_memory / 1024 / 1024
|
||||
memory_readings.append(memory_mb)
|
||||
|
||||
# Get current file size
|
||||
file_size_mb = os.path.getsize(temp_file) / (1024 * 1024)
|
||||
|
||||
print(f'\nIteration {i}:')
|
||||
print(f'Memory usage: {memory_mb:.2f} MB')
|
||||
print(f'File size: {file_size_mb:.2f} MB')
|
||||
|
||||
# Calculate memory growth
|
||||
memory_growth = current_memory - initial_memory
|
||||
growth_percent = (memory_growth / initial_memory) * 100
|
||||
print(
|
||||
f'Memory growth: {memory_growth / 1024 / 1024:.2f} MB ({growth_percent:.1f}%)'
|
||||
)
|
||||
|
||||
# Fail if memory growth is too high
|
||||
assert memory_growth < memory_limit, (
|
||||
f'Memory growth exceeded limit after {i} edits. '
|
||||
f'Growth: {memory_growth / 1024 / 1024:.2f} MB'
|
||||
)
|
||||
|
||||
# Check for consistent growth pattern
|
||||
if len(memory_readings) >= 3:
|
||||
# Calculate growth rate between last 3 readings
|
||||
growth_rate = (memory_readings[-1] - memory_readings[-3]) / 2
|
||||
print(f'Recent growth rate: {growth_rate:.2f} MB per 50 edits')
|
||||
|
||||
# Fail if we see consistent growth above a threshold
|
||||
# Allow more growth for initial allocations
|
||||
max_growth = 2 if i < 100 else 1 # MB per 50 edits
|
||||
if growth_rate > max_growth:
|
||||
pytest.fail(
|
||||
f'Consistent memory growth detected: {growth_rate:.2f} MB '
|
||||
f'per 50 edits after {i} edits'
|
||||
)
|
||||
|
||||
except MemoryError:
|
||||
pytest.fail('Memory limit exceeded - possible memory leak detected')
|
||||
except Exception as e:
|
||||
if 'Cannot allocate memory' in str(e):
|
||||
pytest.fail('Memory limit exceeded - possible memory leak detected')
|
||||
print(f'\nFinal file size: {file_size_mb:.2f} MB')
|
||||
raise
|
||||
|
||||
# Print final statistics
|
||||
print('\nMemory usage statistics:')
|
||||
print(f'Initial memory: {memory_readings[0]:.2f} MB')
|
||||
print(f'Final memory: {memory_readings[-1]:.2f} MB')
|
||||
print(f'Total growth: {(memory_readings[-1] - memory_readings[0]):.2f} MB')
|
||||
print(f'Final file size: {file_size_mb:.2f} MB')
|
||||
@@ -0,0 +1,354 @@
|
||||
"""Integration tests for editor operations with non-UTF-8 encoded files."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor import file_editor
|
||||
from dev_aci.editor.encoding import EncodingManager
|
||||
|
||||
from .conftest import parse_result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_non_utf8_file():
|
||||
"""Create a temporary file with cp1251 encoding for testing."""
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
|
||||
# Create a file with cp1251 encoding containing Russian text
|
||||
with open(path, 'wb') as f:
|
||||
f.write('# -*- coding: cp1251 -*-\n\n'.encode('cp1251'))
|
||||
f.write('# Тестовый файл с кириллицей\n'.encode('cp1251'))
|
||||
f.write('text = "Привет, мир!"\n'.encode('cp1251'))
|
||||
f.write('numbers = [1, 2, 3, 4, 5]\n'.encode('cp1251'))
|
||||
f.write('message = "Это тестовая строка"\n'.encode('cp1251'))
|
||||
|
||||
yield Path(path)
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def test_view_non_utf8_file(temp_non_utf8_file):
|
||||
"""Test viewing a non-UTF-8 encoded file."""
|
||||
# View the file
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_non_utf8_file),
|
||||
)
|
||||
|
||||
# Parse the result
|
||||
result_json = parse_result(result)
|
||||
|
||||
# Verify the content was read correctly
|
||||
assert 'Привет, мир!' in result_json['formatted_output_and_error']
|
||||
assert 'Тестовый файл с кириллицей' in result_json['formatted_output_and_error']
|
||||
assert 'Это тестовая строка' in result_json['formatted_output_and_error']
|
||||
|
||||
|
||||
def test_view_range_non_utf8_file(temp_non_utf8_file):
|
||||
"""Test viewing a specific range of a non-UTF-8 encoded file."""
|
||||
# View only lines 3-5
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_non_utf8_file),
|
||||
view_range=[3, 5],
|
||||
)
|
||||
|
||||
# Parse the result
|
||||
result_json = parse_result(result)
|
||||
|
||||
# Verify the content was read correctly
|
||||
assert 'Тестовый файл с кириллицей' in result_json['formatted_output_and_error']
|
||||
assert 'Привет, мир!' in result_json['formatted_output_and_error']
|
||||
|
||||
# Verify that line 6 is not included
|
||||
assert 'Это тестовая строка' not in result_json['formatted_output_and_error']
|
||||
|
||||
|
||||
def test_str_replace_non_utf8_file(temp_non_utf8_file):
|
||||
"""Test replacing text in a non-UTF-8 encoded file."""
|
||||
# Replace text
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=str(temp_non_utf8_file),
|
||||
old_str='Привет, мир!',
|
||||
new_str='Здравствуй, мир!',
|
||||
enable_linting=False,
|
||||
)
|
||||
|
||||
# Parse the result
|
||||
result_json = parse_result(result)
|
||||
|
||||
# Verify the replacement was successful
|
||||
assert 'Здравствуй, мир!' in result_json['formatted_output_and_error']
|
||||
assert 'Привет, мир!' not in result_json['formatted_output_and_error']
|
||||
|
||||
# Verify the file was saved with the correct encoding
|
||||
with open(temp_non_utf8_file, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
try:
|
||||
decoded = content.decode('cp1251')
|
||||
assert 'Здравствуй, мир!' in decoded
|
||||
except UnicodeDecodeError:
|
||||
pytest.fail('File was not saved with the correct encoding')
|
||||
|
||||
|
||||
def test_insert_non_utf8_file(temp_non_utf8_file):
|
||||
"""Test inserting text in a non-UTF-8 encoded file."""
|
||||
# Insert text after line 4
|
||||
result = file_editor(
|
||||
command='insert',
|
||||
path=str(temp_non_utf8_file),
|
||||
insert_line=4,
|
||||
new_str='new_var = "Новая переменная"',
|
||||
enable_linting=False,
|
||||
)
|
||||
|
||||
# Parse the result
|
||||
result_json = parse_result(result)
|
||||
|
||||
# Verify the insertion was successful
|
||||
assert 'Новая переменная' in result_json['formatted_output_and_error']
|
||||
|
||||
# Verify the file was saved with the correct encoding
|
||||
with open(temp_non_utf8_file, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
try:
|
||||
decoded = content.decode('cp1251')
|
||||
assert 'Новая переменная' in decoded
|
||||
except UnicodeDecodeError:
|
||||
pytest.fail('File was not saved with the correct encoding')
|
||||
|
||||
|
||||
def test_create_non_utf8_file():
|
||||
"""Test creating a new file with non-UTF-8 content."""
|
||||
# Create a temporary path
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
os.unlink(path) # Remove the file so we can create it with the editor
|
||||
|
||||
try:
|
||||
# Create content with Russian characters
|
||||
content = '# -*- coding: cp1251 -*-\n\n'
|
||||
content += '# Новый файл с кириллицей\n'
|
||||
content += 'greeting = "Привет из нового файла!"\n'
|
||||
|
||||
# Create the file
|
||||
result = file_editor(
|
||||
command='create',
|
||||
path=path,
|
||||
file_text=content,
|
||||
enable_linting=False,
|
||||
)
|
||||
|
||||
# Parse the result
|
||||
result_json = parse_result(result)
|
||||
|
||||
# Verify the file was created successfully
|
||||
assert 'File created successfully' in result_json['formatted_output_and_error']
|
||||
|
||||
# Read the file with cp1251 encoding to verify content
|
||||
encoding_manager = EncodingManager()
|
||||
encoding = encoding_manager.detect_encoding(Path(path))
|
||||
|
||||
with open(path, 'r', encoding=encoding) as f:
|
||||
file_content = f.read()
|
||||
|
||||
assert 'Привет из нового файла!' in file_content
|
||||
assert 'Новый файл с кириллицей' in file_content
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def test_undo_edit_non_utf8_file(temp_non_utf8_file):
|
||||
"""Test undoing an edit in a non-UTF-8 encoded file."""
|
||||
# First, make a change
|
||||
file_editor(
|
||||
command='str_replace',
|
||||
path=str(temp_non_utf8_file),
|
||||
old_str='Привет, мир!',
|
||||
new_str='Здравствуй, мир!',
|
||||
enable_linting=False,
|
||||
)
|
||||
|
||||
# Now undo the change
|
||||
result = file_editor(
|
||||
command='undo_edit',
|
||||
path=str(temp_non_utf8_file),
|
||||
enable_linting=False,
|
||||
)
|
||||
|
||||
# Parse the result
|
||||
result_json = parse_result(result)
|
||||
|
||||
# Verify the undo was successful
|
||||
assert 'undone successfully' in result_json['formatted_output_and_error']
|
||||
|
||||
# Verify the original content was restored with the correct encoding
|
||||
with open(temp_non_utf8_file, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
try:
|
||||
decoded = content.decode('cp1251')
|
||||
assert 'Привет, мир!' in decoded
|
||||
assert 'Здравствуй, мир!' not in decoded
|
||||
except UnicodeDecodeError:
|
||||
pytest.fail('File was not restored with the correct encoding')
|
||||
|
||||
|
||||
def test_complex_workflow_non_utf8_file(temp_non_utf8_file):
|
||||
"""Test a complex workflow with multiple operations on a non-UTF-8 encoded file."""
|
||||
# 1. View the file
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_non_utf8_file),
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'Привет, мир!' in result_json['formatted_output_and_error']
|
||||
|
||||
# 2. Replace text
|
||||
result = file_editor(
|
||||
command='str_replace',
|
||||
path=str(temp_non_utf8_file),
|
||||
old_str='Привет, мир!',
|
||||
new_str='Здравствуй, мир!',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'Здравствуй, мир!' in result_json['formatted_output_and_error']
|
||||
|
||||
# 3. Insert text
|
||||
result = file_editor(
|
||||
command='insert',
|
||||
path=str(temp_non_utf8_file),
|
||||
insert_line=5,
|
||||
new_str='# Добавленная строка\nboolean_var = True',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'Добавленная строка' in result_json['formatted_output_and_error']
|
||||
|
||||
# 4. View specific range
|
||||
result = file_editor(
|
||||
command='view',
|
||||
path=str(temp_non_utf8_file),
|
||||
view_range=[5, 7],
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'Добавленная строка' in result_json['formatted_output_and_error']
|
||||
assert 'boolean_var = True' in result_json['formatted_output_and_error']
|
||||
|
||||
# 5. Undo the last edit
|
||||
result = file_editor(
|
||||
command='undo_edit',
|
||||
path=str(temp_non_utf8_file),
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json = parse_result(result)
|
||||
assert 'undone successfully' in result_json['formatted_output_and_error']
|
||||
|
||||
# 6. Verify the file content after all operations
|
||||
with open(temp_non_utf8_file, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
try:
|
||||
decoded = content.decode('cp1251')
|
||||
assert 'Здравствуй, мир!' in decoded # From step 2
|
||||
assert 'Добавленная строка' not in decoded # Undone in step 5
|
||||
except UnicodeDecodeError:
|
||||
pytest.fail('File was not maintained with the correct encoding')
|
||||
|
||||
|
||||
def test_mixed_encoding_workflow():
|
||||
"""Test workflow with files of different encodings."""
|
||||
# Create two temporary files with different encodings
|
||||
fd1, path1 = tempfile.mkstemp()
|
||||
fd2, path2 = tempfile.mkstemp()
|
||||
os.close(fd1)
|
||||
os.close(fd2)
|
||||
|
||||
try:
|
||||
# Create a cp1251 encoded file
|
||||
with open(path1, 'wb') as f:
|
||||
f.write('# -*- coding: cp1251 -*-\n'.encode('cp1251'))
|
||||
f.write('text_cp1251 = "Текст в кодировке CP1251"\n'.encode('cp1251'))
|
||||
|
||||
# Create a UTF-8 encoded file
|
||||
with open(path2, 'w', encoding='utf-8') as f:
|
||||
f.write('# -*- coding: utf-8 -*-\n')
|
||||
f.write('text_utf8 = "Текст в кодировке UTF-8"\n')
|
||||
|
||||
# 1. View the cp1251 file
|
||||
result1 = file_editor(
|
||||
command='view',
|
||||
path=path1,
|
||||
)
|
||||
result_json1 = parse_result(result1)
|
||||
assert 'Текст в кодировке CP1251' in result_json1['formatted_output_and_error']
|
||||
|
||||
# 2. View the UTF-8 file
|
||||
result2 = file_editor(
|
||||
command='view',
|
||||
path=path2,
|
||||
)
|
||||
result_json2 = parse_result(result2)
|
||||
assert 'Текст в кодировке UTF-8' in result_json2['formatted_output_and_error']
|
||||
|
||||
# 3. Edit the cp1251 file
|
||||
result3 = file_editor(
|
||||
command='str_replace',
|
||||
path=path1,
|
||||
old_str='Текст в кодировке CP1251',
|
||||
new_str='Измененный текст в CP1251',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json3 = parse_result(result3)
|
||||
assert 'Измененный текст в CP1251' in result_json3['formatted_output_and_error']
|
||||
|
||||
# 4. Edit the UTF-8 file
|
||||
result4 = file_editor(
|
||||
command='str_replace',
|
||||
path=path2,
|
||||
old_str='Текст в кодировке UTF-8',
|
||||
new_str='Измененный текст в UTF-8',
|
||||
enable_linting=False,
|
||||
)
|
||||
result_json4 = parse_result(result4)
|
||||
assert 'Измененный текст в UTF-8' in result_json4['formatted_output_and_error']
|
||||
|
||||
# 5. Verify both files maintain their original encodings
|
||||
with open(path1, 'rb') as f:
|
||||
content1 = f.read()
|
||||
with open(path2, 'rb') as f:
|
||||
content2 = f.read()
|
||||
|
||||
# CP1251 file should be decodable with CP1251
|
||||
try:
|
||||
decoded1 = content1.decode('cp1251')
|
||||
assert 'Измененный текст в CP1251' in decoded1
|
||||
except UnicodeDecodeError:
|
||||
pytest.fail('CP1251 file was not saved with the correct encoding')
|
||||
|
||||
# UTF-8 file should be decodable with UTF-8
|
||||
try:
|
||||
decoded2 = content2.decode('utf-8')
|
||||
assert 'Измененный текст в UTF-8' in decoded2
|
||||
except UnicodeDecodeError:
|
||||
pytest.fail('UTF-8 file was not saved with the correct encoding')
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
try:
|
||||
os.unlink(path1)
|
||||
os.unlink(path2)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Tests for peak memory usage in file operations."""
|
||||
|
||||
import os
|
||||
import resource
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor import file_editor
|
||||
|
||||
|
||||
def get_memory_info():
|
||||
"""Get current and peak memory usage in bytes."""
|
||||
process = psutil.Process(os.getpid())
|
||||
rss = process.memory_info().rss
|
||||
peak_rss = (
|
||||
resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024
|
||||
) # Convert KB to bytes
|
||||
return {
|
||||
'rss': rss,
|
||||
'peak_rss': peak_rss,
|
||||
'max': max(rss, peak_rss),
|
||||
}
|
||||
|
||||
|
||||
def create_test_file(path: Path, size_mb: float = 5.0):
|
||||
"""Create a test file of given size (default: 5MB)."""
|
||||
line_size = 100 # bytes per line approximately
|
||||
num_lines = int((size_mb * 1024 * 1024) // line_size)
|
||||
|
||||
print(f'\nCreating test file with {num_lines} lines...')
|
||||
with open(path, 'w') as f:
|
||||
for i in range(num_lines):
|
||||
f.write(f'Line {i}: ' + 'x' * (line_size - 10) + '\n')
|
||||
|
||||
actual_size = os.path.getsize(path)
|
||||
print(f'File created, size: {actual_size / 1024 / 1024:.2f} MB')
|
||||
return actual_size
|
||||
|
||||
|
||||
def set_memory_limit(file_size: int, multiplier: float = 2.0):
|
||||
"""Set memory limit to multiplier * file_size."""
|
||||
# Add base memory for pytest and other processes (100MB)
|
||||
base_memory = 100 * 1024 * 1024 # 100MB
|
||||
memory_limit = int(file_size * multiplier + base_memory)
|
||||
try:
|
||||
# Get current limits
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
|
||||
# Only set limit if it's higher than current usage
|
||||
current_usage = psutil.Process().memory_info().rss
|
||||
if memory_limit > current_usage:
|
||||
resource.setrlimit(resource.RLIMIT_AS, (memory_limit, hard))
|
||||
print(f'Memory limit set to {memory_limit / 1024 / 1024:.2f} MB')
|
||||
else:
|
||||
print(
|
||||
f'Warning: Current memory usage ({current_usage / 1024 / 1024:.2f} MB) higher than limit ({memory_limit / 1024 / 1024:.2f} MB)'
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Warning: Could not set memory limit: {str(e)}')
|
||||
return memory_limit
|
||||
|
||||
|
||||
def check_memory_usage(initial_memory: int, file_size: int, operation: str):
|
||||
"""Check if memory usage is within acceptable limits."""
|
||||
current = get_memory_info()
|
||||
memory_growth = current['max'] - initial_memory
|
||||
print(f'Peak memory growth: {memory_growth / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Memory growth should be reasonable
|
||||
# Allow up to 2x file size for temporary buffers plus 50MB for Python overhead
|
||||
overhead = 50 * 1024 * 1024 # 50MB
|
||||
max_growth = int(file_size * 2 + overhead)
|
||||
assert memory_growth < max_growth, (
|
||||
f'Peak memory growth too high for {operation}: {memory_growth / 1024 / 1024:.2f} MB '
|
||||
f'(limit: {max_growth / 1024 / 1024:.2f} MB)'
|
||||
)
|
||||
|
||||
|
||||
def test_str_replace_peak_memory():
|
||||
"""Test that str_replace operation has reasonable peak memory usage."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
file_size = create_test_file(path)
|
||||
|
||||
# Force Python to release file handles and clear buffers
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
# Get initial memory usage
|
||||
initial = get_memory_info()
|
||||
print(f'Initial memory usage: {initial["rss"] / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Set memory limit
|
||||
set_memory_limit(file_size)
|
||||
|
||||
# Perform str_replace operation
|
||||
try:
|
||||
_ = file_editor(
|
||||
command='str_replace',
|
||||
path=path,
|
||||
old_str='Line 5000', # Replace a line in the middle
|
||||
new_str='Modified line',
|
||||
enable_linting=False,
|
||||
)
|
||||
except MemoryError:
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
except Exception as e:
|
||||
if 'Cannot allocate memory' in str(e):
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
raise
|
||||
|
||||
check_memory_usage(initial['max'], file_size, 'str_replace')
|
||||
|
||||
|
||||
def test_insert_peak_memory():
|
||||
"""Test that insert operation has reasonable peak memory usage."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
file_size = create_test_file(path)
|
||||
|
||||
# Force Python to release file handles and clear buffers
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
# Get initial memory usage
|
||||
initial = get_memory_info()
|
||||
print(f'Initial memory usage: {initial["rss"] / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Set memory limit
|
||||
set_memory_limit(file_size)
|
||||
|
||||
# Perform insert operation
|
||||
try:
|
||||
_ = file_editor(
|
||||
command='insert',
|
||||
path=path,
|
||||
insert_line=5000, # Insert in the middle
|
||||
new_str='New line inserted\n' * 10,
|
||||
enable_linting=False,
|
||||
)
|
||||
except MemoryError:
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
except Exception as e:
|
||||
if 'Cannot allocate memory' in str(e):
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
raise
|
||||
|
||||
check_memory_usage(initial['max'], file_size, 'insert')
|
||||
|
||||
|
||||
def test_view_peak_memory():
|
||||
"""Test that view operation has reasonable peak memory usage."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
file_size = create_test_file(path)
|
||||
|
||||
# Force Python to release file handles and clear buffers
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
# Get initial memory usage
|
||||
initial = get_memory_info()
|
||||
print(f'Initial memory usage: {initial["rss"] / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Set memory limit
|
||||
set_memory_limit(file_size)
|
||||
|
||||
# Test viewing specific lines
|
||||
try:
|
||||
_ = file_editor(
|
||||
command='view',
|
||||
path=path,
|
||||
view_range=[5000, 5100], # View 100 lines from middle
|
||||
enable_linting=False,
|
||||
)
|
||||
except MemoryError:
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
except Exception as e:
|
||||
if 'Cannot allocate memory' in str(e):
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
raise
|
||||
|
||||
check_memory_usage(initial['max'], file_size, 'view')
|
||||
|
||||
|
||||
def test_view_full_file_peak_memory():
|
||||
"""Test that viewing entire file has reasonable peak memory usage."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
file_size = create_test_file(path, size_mb=5.0) # Smaller file for full view
|
||||
|
||||
# Force Python to release file handles and clear buffers
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
# Get initial memory usage
|
||||
initial = get_memory_info()
|
||||
print(f'Initial memory usage: {initial["rss"] / 1024 / 1024:.2f} MB')
|
||||
|
||||
# Set memory limit
|
||||
set_memory_limit(file_size)
|
||||
|
||||
# Test viewing entire file
|
||||
try:
|
||||
_ = file_editor(
|
||||
command='view',
|
||||
path=path,
|
||||
enable_linting=False,
|
||||
)
|
||||
except MemoryError:
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
except Exception as e:
|
||||
if 'Cannot allocate memory' in str(e):
|
||||
pytest.fail('Memory limit exceeded - peak memory usage too high')
|
||||
raise
|
||||
|
||||
check_memory_usage(initial['max'], file_size, 'view_full')
|
||||
|
||||
|
||||
def test_large_history_insert():
|
||||
"""Test inserting a large amount of data into the history cache."""
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from dev_aci.editor.history import FileHistoryManager
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
history_dir = Path(temp_dir)
|
||||
manager = FileHistoryManager(max_history_per_file=1000, history_dir=history_dir)
|
||||
|
||||
# Create a large string (about 1MB)
|
||||
large_content = 'x' * (1024 * 1024)
|
||||
|
||||
# Try to insert the large content multiple times
|
||||
num_files = 100
|
||||
for i in range(num_files):
|
||||
try:
|
||||
manager.add_history(Path(f'test_file_{i}.txt'), large_content)
|
||||
except Exception as e:
|
||||
pytest.fail(f'Error occurred on iteration {i}: {str(e)}')
|
||||
|
||||
# Check if we can still retrieve the last entry
|
||||
last_content = manager.pop_last_history(Path(f'test_file_{num_files - 1}.txt'))
|
||||
assert (
|
||||
last_content == large_content
|
||||
), 'Failed to retrieve the last inserted content'
|
||||
|
||||
# Check if the number of cache entries is correct
|
||||
cache_entries = list(manager.cache)
|
||||
assert (
|
||||
len(cache_entries)
|
||||
== num_files * 2
|
||||
- 1 # The cache entry for file content was removed, only metadata remains
|
||||
), f'Expected {num_files * 2 - 1} cache entries ({num_files - 1} content + {num_files} metadata), but found {len(cache_entries)}'
|
||||
@@ -0,0 +1,607 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor.editor import OHEditor
|
||||
from dev_aci.editor.exceptions import (
|
||||
EditorToolParameterInvalidError,
|
||||
EditorToolParameterMissingError,
|
||||
ToolError,
|
||||
)
|
||||
from dev_aci.editor.prompts import (
|
||||
DIRECTORY_CONTENT_TRUNCATED_NOTICE,
|
||||
FILE_CONTENT_TRUNCATED_NOTICE,
|
||||
)
|
||||
from dev_aci.editor.results import CLIResult, ToolResult
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor(tmp_path):
|
||||
editor = OHEditor()
|
||||
# Set up a temporary directory with test files
|
||||
test_file = tmp_path / 'test.txt'
|
||||
test_file.write_text('This is a test file.\nThis file is for testing purposes.')
|
||||
return editor, test_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor_python_file_with_tabs(tmp_path):
|
||||
editor = OHEditor()
|
||||
# Set up a temporary directory with test files
|
||||
test_file = tmp_path / 'test.py'
|
||||
test_file.write_text('def test():\n\tprint("Hello, World!")')
|
||||
return editor, test_file
|
||||
|
||||
|
||||
def test_view_file(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(command='view', path=str(test_file))
|
||||
assert isinstance(result, CLIResult)
|
||||
assert f"Here's the result of running `cat -n` on {test_file}:" in result.output
|
||||
assert '1\tThis is a test file.' in result.output
|
||||
assert '2\tThis file is for testing purposes.' in result.output
|
||||
|
||||
|
||||
def test_view_directory(editor):
|
||||
editor, test_file = editor
|
||||
parent_dir = test_file.parent
|
||||
result = editor(command='view', path=str(parent_dir))
|
||||
assert (
|
||||
result.output
|
||||
== f"""Here's the files and directories up to 2 levels deep in {parent_dir}, excluding hidden items:
|
||||
{parent_dir}/
|
||||
{parent_dir}/test.txt"""
|
||||
)
|
||||
|
||||
|
||||
def test_create_file(editor):
|
||||
editor, test_file = editor
|
||||
new_file = test_file.parent / 'new_file.txt'
|
||||
result = editor(command='create', path=str(new_file), file_text='New file content')
|
||||
assert isinstance(result, ToolResult)
|
||||
assert new_file.exists()
|
||||
assert new_file.read_text() == 'New file content'
|
||||
assert 'File created successfully' in result.output
|
||||
|
||||
|
||||
def test_create_with_empty_string(editor):
|
||||
editor, test_file = editor
|
||||
new_file = test_file.parent / 'empty_content.txt'
|
||||
result = editor(command='create', path=str(new_file), file_text='')
|
||||
assert isinstance(result, ToolResult)
|
||||
assert new_file.exists()
|
||||
assert new_file.read_text() == ''
|
||||
assert 'File created successfully' in result.output
|
||||
|
||||
|
||||
def test_create_with_none_file_text(editor):
|
||||
editor, test_file = editor
|
||||
new_file = test_file.parent / 'none_content.txt'
|
||||
with pytest.raises(EditorToolParameterMissingError) as exc_info:
|
||||
editor(command='create', path=str(new_file), file_text=None)
|
||||
assert 'file_text' in str(exc_info.value.message)
|
||||
|
||||
|
||||
def test_str_replace_no_linting(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='test file',
|
||||
new_str='sample file',
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
|
||||
# Test str_replace command
|
||||
assert (
|
||||
result.output
|
||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
|
||||
1\tThis is a sample file.
|
||||
2\tThis file is for testing purposes.
|
||||
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
||||
)
|
||||
|
||||
# Test that the file content has been updated
|
||||
assert 'This is a sample file.' in test_file.read_text()
|
||||
|
||||
|
||||
def test_str_replace_multi_line_no_linting(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='This is a test file.\nThis file is for testing purposes.',
|
||||
new_str='This is a sample file.\nThis file is for testing purposes.',
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
|
||||
# Test str_replace command
|
||||
assert (
|
||||
result.output
|
||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
|
||||
1\tThis is a sample file.
|
||||
2\tThis file is for testing purposes.
|
||||
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
||||
)
|
||||
|
||||
|
||||
def test_str_replace_multi_line_with_tabs_no_linting(editor_python_file_with_tabs):
|
||||
editor, test_file = editor_python_file_with_tabs
|
||||
result = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='def test():\n\tprint("Hello, World!")',
|
||||
new_str='def test():\n\tprint("Hello, Universe!")',
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
|
||||
assert (
|
||||
result.output
|
||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
|
||||
1\tdef test():
|
||||
2\t{'\t'.expandtabs()}print("Hello, Universe!")
|
||||
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
||||
)
|
||||
|
||||
|
||||
def test_str_replace_with_linting(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='test file',
|
||||
new_str='sample file',
|
||||
enable_linting=True,
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
|
||||
# Test str_replace command
|
||||
assert (
|
||||
result.output
|
||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
|
||||
1\tThis is a sample file.
|
||||
2\tThis file is for testing purposes.
|
||||
|
||||
No linting issues found in the changes.
|
||||
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
||||
)
|
||||
|
||||
# Test that the file content has been updated
|
||||
assert 'This is a sample file.' in test_file.read_text()
|
||||
|
||||
|
||||
def test_str_replace_error_multiple_occurrences(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
editor(
|
||||
command='str_replace', path=str(test_file), old_str='test', new_str='sample'
|
||||
)
|
||||
assert 'Multiple occurrences of old_str `test`' in str(exc_info.value.message)
|
||||
assert '[1, 2]' in str(exc_info.value.message) # Should show both line numbers
|
||||
|
||||
|
||||
def test_str_replace_error_multiple_multiline_occurrences(editor):
|
||||
editor, test_file = editor
|
||||
# Create a file with two identical multi-line blocks
|
||||
multi_block = """def example():
|
||||
print("Hello")
|
||||
return True"""
|
||||
content = f"{multi_block}\n\nprint('separator')\n\n{multi_block}"
|
||||
test_file.write_text(content)
|
||||
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str=multi_block,
|
||||
new_str='def new():\n print("World")',
|
||||
)
|
||||
error_msg = str(exc_info.value.message)
|
||||
assert 'Multiple occurrences of old_str' in error_msg
|
||||
assert '[1, 7]' in error_msg # Should show correct starting line numbers
|
||||
|
||||
|
||||
def test_str_replace_nonexistent_string(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='Non-existent Line',
|
||||
new_str='New Line',
|
||||
)
|
||||
assert 'No replacement was performed' in str(exc_info)
|
||||
assert f'old_str `Non-existent Line` did not appear verbatim in {test_file}' in str(
|
||||
exc_info.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_str_replace_with_empty_new_str(editor):
|
||||
editor, test_file = editor
|
||||
test_file.write_text('Line 1\nLine to remove\nLine 3')
|
||||
result = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='Line to remove\n',
|
||||
new_str='',
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
assert test_file.read_text() == 'Line 1\nLine 3'
|
||||
|
||||
|
||||
def test_str_replace_with_empty_old_str(editor):
|
||||
editor, test_file = editor
|
||||
test_file.write_text('Line 1\nLine 2\nLine 3')
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='',
|
||||
new_str='New string',
|
||||
)
|
||||
assert (
|
||||
str(exc_info.value.message)
|
||||
== """No replacement was performed. Multiple occurrences of old_str `` in lines [1, 2, 3]. Please ensure it is unique."""
|
||||
)
|
||||
|
||||
|
||||
def test_str_replace_with_none_old_str(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterMissingError) as exc_info:
|
||||
editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str=None,
|
||||
new_str='new content',
|
||||
)
|
||||
assert 'old_str' in str(exc_info.value.message)
|
||||
|
||||
|
||||
def test_insert_no_linting(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(
|
||||
command='insert', path=str(test_file), insert_line=1, new_str='Inserted line'
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
assert 'Inserted line' in test_file.read_text()
|
||||
print(result.output)
|
||||
assert (
|
||||
result.output
|
||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
1\tThis is a test file.
|
||||
2\tInserted line
|
||||
3\tThis file is for testing purposes.
|
||||
Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary."""
|
||||
)
|
||||
|
||||
|
||||
def test_insert_with_linting(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(
|
||||
command='insert',
|
||||
path=str(test_file),
|
||||
insert_line=1,
|
||||
new_str='Inserted line',
|
||||
enable_linting=True,
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
assert 'Inserted line' in test_file.read_text()
|
||||
print(result.output)
|
||||
assert (
|
||||
result.output
|
||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
1\tThis is a test file.
|
||||
2\tInserted line
|
||||
3\tThis file is for testing purposes.
|
||||
|
||||
No linting issues found in the changes.
|
||||
Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary."""
|
||||
)
|
||||
|
||||
|
||||
def test_insert_invalid_line(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(
|
||||
command='insert',
|
||||
path=str(test_file),
|
||||
insert_line=10,
|
||||
new_str='Invalid Insert',
|
||||
)
|
||||
assert 'Invalid `insert_line` parameter' in str(exc_info.value.message)
|
||||
assert 'It should be within the range of lines of the file' in str(
|
||||
exc_info.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_insert_with_empty_string(editor):
|
||||
editor, test_file = editor
|
||||
result = editor(
|
||||
command='insert',
|
||||
path=str(test_file),
|
||||
insert_line=1,
|
||||
new_str='',
|
||||
)
|
||||
assert isinstance(result, CLIResult)
|
||||
content = test_file.read_text().splitlines()
|
||||
assert '' in content
|
||||
assert len(content) == 3 # Original 2 lines plus empty line
|
||||
|
||||
|
||||
def test_insert_with_none_new_str(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterMissingError) as exc_info:
|
||||
editor(
|
||||
command='insert',
|
||||
path=str(test_file),
|
||||
insert_line=1,
|
||||
new_str=None,
|
||||
)
|
||||
assert 'new_str' in str(exc_info.value.message)
|
||||
|
||||
|
||||
def test_undo_edit(editor):
|
||||
editor, test_file = editor
|
||||
# Make an edit to be undone
|
||||
result = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='test file',
|
||||
new_str='sample file',
|
||||
)
|
||||
# Undo the edit
|
||||
result = editor(command='undo_edit', path=str(test_file))
|
||||
assert isinstance(result, CLIResult)
|
||||
assert 'Last edit to' in result.output
|
||||
assert 'test file' in test_file.read_text() # Original content restored
|
||||
|
||||
|
||||
def test_multiple_undo_edits(editor):
|
||||
editor, test_file = editor
|
||||
# Make an edit to be undone
|
||||
_ = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='test file',
|
||||
new_str='sample file v1',
|
||||
)
|
||||
# Make another edit to be undone
|
||||
_ = editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='sample file v1',
|
||||
new_str='sample file v2',
|
||||
)
|
||||
# Undo the last edit
|
||||
result = editor(command='undo_edit', path=str(test_file))
|
||||
assert isinstance(result, CLIResult)
|
||||
assert 'Last edit to' in result.output
|
||||
assert 'sample file v1' in test_file.read_text() # Previous content restored
|
||||
|
||||
# Undo the first edit
|
||||
result = editor(command='undo_edit', path=str(test_file))
|
||||
assert isinstance(result, CLIResult)
|
||||
assert 'Last edit to' in result.output
|
||||
assert 'test file' in test_file.read_text() # Original content restored
|
||||
|
||||
|
||||
def test_validate_path_invalid(editor):
|
||||
editor, test_file = editor
|
||||
invalid_file = test_file.parent / 'nonexistent.txt'
|
||||
with pytest.raises(EditorToolParameterInvalidError):
|
||||
editor(command='view', path=str(invalid_file))
|
||||
|
||||
|
||||
def test_create_existing_file_error(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterInvalidError):
|
||||
editor(command='create', path=str(test_file), file_text='New content')
|
||||
|
||||
|
||||
def test_str_replace_missing_old_str(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterMissingError):
|
||||
editor(command='str_replace', path=str(test_file), new_str='sample')
|
||||
|
||||
|
||||
def test_str_replace_new_str_and_old_str_same(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(
|
||||
command='str_replace',
|
||||
path=str(test_file),
|
||||
old_str='test file',
|
||||
new_str='test file',
|
||||
)
|
||||
assert (
|
||||
'No replacement was performed. `new_str` and `old_str` must be different.'
|
||||
in str(exc_info.value.message)
|
||||
)
|
||||
|
||||
|
||||
def test_insert_missing_line_param(editor):
|
||||
editor, test_file = editor
|
||||
with pytest.raises(EditorToolParameterMissingError):
|
||||
editor(command='insert', path=str(test_file), new_str='Missing insert line')
|
||||
|
||||
|
||||
def test_undo_edit_no_history_error(editor):
|
||||
editor, test_file = editor
|
||||
empty_file = test_file.parent / 'empty.txt'
|
||||
empty_file.write_text('')
|
||||
with pytest.raises(ToolError):
|
||||
editor(command='undo_edit', path=str(empty_file))
|
||||
|
||||
|
||||
def test_view_directory_with_hidden_files(tmp_path):
|
||||
editor = OHEditor()
|
||||
|
||||
# Create a directory with some test files
|
||||
test_dir = tmp_path / 'test_dir'
|
||||
test_dir.mkdir()
|
||||
(test_dir / 'visible.txt').write_text('content1')
|
||||
(test_dir / '.hidden1').write_text('hidden1')
|
||||
(test_dir / '.hidden2').write_text('hidden2')
|
||||
|
||||
# Create a hidden subdirectory with a file
|
||||
hidden_subdir = test_dir / '.hidden_dir'
|
||||
hidden_subdir.mkdir()
|
||||
(hidden_subdir / 'file.txt').write_text('content3')
|
||||
|
||||
# Create a visible subdirectory
|
||||
visible_subdir = test_dir / 'visible_dir'
|
||||
visible_subdir.mkdir()
|
||||
|
||||
# View the directory
|
||||
result = editor(command='view', path=str(test_dir))
|
||||
|
||||
# Verify output
|
||||
assert isinstance(result, CLIResult)
|
||||
assert str(test_dir) in result.output
|
||||
assert 'visible.txt' in result.output # Visible file is shown
|
||||
assert 'visible_dir' in result.output # Visible directory is shown
|
||||
assert '.hidden1' not in result.output # Hidden files not shown
|
||||
assert '.hidden2' not in result.output
|
||||
assert '.hidden_dir' not in result.output
|
||||
assert (
|
||||
'3 hidden files/directories in this directory are excluded' in result.output
|
||||
) # Shows count of hidden items in current dir only
|
||||
assert 'ls -la' in result.output # Shows command to view hidden files
|
||||
|
||||
|
||||
def test_view_symlinked_directory(tmp_path):
|
||||
editor = OHEditor()
|
||||
|
||||
# Create a directory with some test files
|
||||
source_dir = tmp_path / 'source_dir'
|
||||
source_dir.mkdir()
|
||||
(source_dir / 'file1.txt').write_text('content1')
|
||||
(source_dir / 'file2.txt').write_text('content2')
|
||||
|
||||
# Create a subdirectory with a file
|
||||
subdir = source_dir / 'subdir'
|
||||
subdir.mkdir()
|
||||
(subdir / 'file3.txt').write_text('content3')
|
||||
|
||||
# Create a symlink to the directory
|
||||
symlink_dir = tmp_path / 'symlink_dir'
|
||||
symlink_dir.symlink_to(source_dir)
|
||||
|
||||
# View the symlinked directory
|
||||
result = editor(command='view', path=str(symlink_dir))
|
||||
|
||||
# Verify that all files are listed through the symlink
|
||||
assert isinstance(result, CLIResult)
|
||||
assert str(symlink_dir) in result.output
|
||||
assert 'file1.txt' in result.output
|
||||
assert 'file2.txt' in result.output
|
||||
assert 'subdir' in result.output
|
||||
assert 'file3.txt' in result.output
|
||||
|
||||
|
||||
def test_view_large_directory_with_truncation(editor, tmp_path):
|
||||
editor, _ = editor
|
||||
# Create a directory with many files to trigger truncation
|
||||
large_dir = tmp_path / 'large_dir'
|
||||
large_dir.mkdir()
|
||||
for i in range(1000): # 1000 files should trigger truncation
|
||||
(large_dir / f'file_{i}.txt').write_text('content')
|
||||
|
||||
result = editor(command='view', path=str(large_dir))
|
||||
assert isinstance(result, CLIResult)
|
||||
assert DIRECTORY_CONTENT_TRUNCATED_NOTICE in result.output
|
||||
|
||||
|
||||
def test_view_directory_on_hidden_path(tmp_path):
|
||||
"""Directory structure:
|
||||
.test_dir/
|
||||
├── visible1.txt
|
||||
├── .hidden1
|
||||
├── visible_dir/
|
||||
│ ├── visible2.txt
|
||||
│ └── .hidden2
|
||||
└── .hidden_dir/
|
||||
├── visible3.txt
|
||||
└── .hidden3
|
||||
"""
|
||||
|
||||
editor = OHEditor()
|
||||
|
||||
# Create a directory with test files at depth 1
|
||||
hidden_test_dir = tmp_path / '.hidden_test_dir'
|
||||
hidden_test_dir.mkdir()
|
||||
(hidden_test_dir / 'visible1.txt').write_text('content1')
|
||||
(hidden_test_dir / '.hidden1').write_text('hidden1')
|
||||
|
||||
# Create a visible subdirectory with visible and hidden files
|
||||
visible_subdir = hidden_test_dir / 'visible_dir'
|
||||
visible_subdir.mkdir()
|
||||
(visible_subdir / 'visible2.txt').write_text('content2')
|
||||
(visible_subdir / '.hidden2').write_text('hidden2')
|
||||
|
||||
# Create a hidden subdirectory with visible and hidden files
|
||||
hidden_subdir = hidden_test_dir / '.hidden_dir'
|
||||
hidden_subdir.mkdir()
|
||||
(hidden_subdir / 'visible3.txt').write_text('content3')
|
||||
(hidden_subdir / '.hidden3').write_text('hidden3')
|
||||
|
||||
# View the directory
|
||||
result = editor(command='view', path=str(hidden_test_dir))
|
||||
|
||||
# Verify output
|
||||
assert isinstance(result, CLIResult)
|
||||
# Depth 1: Visible files/dirs shown, hidden files/dirs not shown
|
||||
assert 'visible1.txt' in result.output
|
||||
assert 'visible_dir' in result.output
|
||||
assert '.hidden1' not in result.output
|
||||
assert '.hidden_dir' not in result.output
|
||||
|
||||
# Depth 2: Files in visible_dir shown
|
||||
assert 'visible2.txt' in result.output
|
||||
assert '.hidden2' not in result.output
|
||||
|
||||
# Depth 2: Files in hidden_dir not shown
|
||||
assert 'visible3.txt' not in result.output
|
||||
assert '.hidden3' not in result.output
|
||||
|
||||
# Hidden file count only includes depth 1
|
||||
assert (
|
||||
'2 hidden files/directories in this directory are excluded' in result.output
|
||||
) # Only .hidden1 and .hidden_dir at depth 1
|
||||
|
||||
|
||||
def test_view_large_file_with_truncation(editor, tmp_path):
|
||||
editor, _ = editor
|
||||
# Create a large file to trigger truncation
|
||||
large_file = tmp_path / 'large_test.txt'
|
||||
large_content = 'Line 1\n' * 16000 # 16000 lines should trigger truncation
|
||||
large_file.write_text(large_content)
|
||||
|
||||
result = editor(command='view', path=str(large_file))
|
||||
assert isinstance(result, CLIResult)
|
||||
assert FILE_CONTENT_TRUNCATED_NOTICE in result.output
|
||||
|
||||
|
||||
def test_validate_path_suggests_absolute_path(editor, tmp_path):
|
||||
editor, test_file = editor
|
||||
|
||||
# Since the editor fixture doesn't set workspace_root, we should not get a suggestion
|
||||
relative_path = test_file.name # This is a relative path
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(command='view', path=relative_path)
|
||||
error_message = str(exc_info.value.message)
|
||||
assert 'The path should be an absolute path' in error_message
|
||||
assert 'Maybe you meant' not in error_message
|
||||
|
||||
# Now create an editor with workspace_root
|
||||
workspace_editor = OHEditor(workspace_root=str(test_file.parent))
|
||||
|
||||
# We should get a suggestion now
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
workspace_editor(command='view', path=relative_path)
|
||||
error_message = str(exc_info.value.message)
|
||||
assert 'The path should be an absolute path' in error_message
|
||||
assert 'Maybe you meant' in error_message
|
||||
suggested_path = error_message.split('Maybe you meant ')[1].strip('?')
|
||||
assert Path(suggested_path).is_absolute()
|
||||
assert str(test_file.parent) in suggested_path
|
||||
@@ -0,0 +1,253 @@
|
||||
"""Unit tests for the encoding module."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from cachetools import LRUCache
|
||||
|
||||
from dev_aci.editor.encoding import EncodingManager, with_encoding
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
"""Create a temporary file for testing."""
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
yield Path(path)
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def encoding_manager():
|
||||
"""Create an EncodingManager instance for testing."""
|
||||
return EncodingManager()
|
||||
|
||||
|
||||
def test_init(encoding_manager):
|
||||
"""Test initialization of EncodingManager."""
|
||||
assert isinstance(encoding_manager, EncodingManager)
|
||||
assert isinstance(encoding_manager._encoding_cache, LRUCache)
|
||||
assert encoding_manager.default_encoding == 'utf-8'
|
||||
assert encoding_manager.confidence_threshold == 0.9
|
||||
|
||||
|
||||
def test_detect_encoding_nonexistent_file(encoding_manager):
|
||||
"""Test detecting encoding for a nonexistent file."""
|
||||
nonexistent_path = Path('/nonexistent/file.txt')
|
||||
encoding = encoding_manager.detect_encoding(nonexistent_path)
|
||||
assert encoding == encoding_manager.default_encoding
|
||||
|
||||
|
||||
def test_detect_encoding_utf8(encoding_manager, temp_file):
|
||||
"""Test detecting UTF-8 encoding."""
|
||||
# Create a UTF-8 encoded file
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write('Hello, world! UTF-8 encoded text.')
|
||||
|
||||
encoding = encoding_manager.detect_encoding(temp_file)
|
||||
assert encoding.lower() in ('utf-8', 'ascii')
|
||||
|
||||
|
||||
def test_detect_encoding_utf8_with_icon(encoding_manager, temp_file):
|
||||
"""Test detecting UTF-8 encoding with a word and an emoji."""
|
||||
# Create a UTF-8 encoded file with a single word and an emoji
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write('Hello 😊')
|
||||
|
||||
encoding = encoding_manager.detect_encoding(temp_file)
|
||||
assert encoding.lower() == 'utf-8'
|
||||
|
||||
|
||||
def test_detect_encoding_cp1251(encoding_manager, temp_file):
|
||||
"""Test detecting CP1251 encoding."""
|
||||
# Create a CP1251 encoded file with Cyrillic characters
|
||||
with open(temp_file, 'wb') as f:
|
||||
f.write('Привет, мир! Текст в кодировке CP1251.'.encode('cp1251'))
|
||||
|
||||
encoding = encoding_manager.detect_encoding(temp_file)
|
||||
assert encoding.lower() in ('windows-1251', 'cp1251')
|
||||
|
||||
|
||||
def test_detect_encoding_low_confidence(encoding_manager, temp_file):
|
||||
"""Test fallback to default encoding when confidence is low."""
|
||||
# Create a file with mixed encodings to confuse the detector
|
||||
with open(temp_file, 'wb') as f:
|
||||
f.write(b'\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f')
|
||||
|
||||
# Mock chardet.detect to return low confidence
|
||||
with patch(
|
||||
'charset_normalizer.detect',
|
||||
return_value={'encoding': 'ascii', 'confidence': 0.3},
|
||||
):
|
||||
encoding = encoding_manager.detect_encoding(temp_file)
|
||||
assert encoding == encoding_manager.default_encoding
|
||||
|
||||
|
||||
def test_detect_encoding_none_result(encoding_manager, temp_file):
|
||||
"""Test fallback to default encoding when chardet returns None for encoding."""
|
||||
with open(temp_file, 'wb') as f:
|
||||
f.write(b'\x00\x01\x02\x03') # Binary data
|
||||
|
||||
# Mock chardet.detect to return None for encoding
|
||||
with patch(
|
||||
'charset_normalizer.detect', return_value={'encoding': None, 'confidence': 0.0}
|
||||
):
|
||||
encoding = encoding_manager.detect_encoding(temp_file)
|
||||
assert encoding == encoding_manager.default_encoding
|
||||
|
||||
|
||||
def test_get_encoding_cache_hit(encoding_manager, temp_file):
|
||||
"""Test that get_encoding uses cached values when available."""
|
||||
# Create a file
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write('Hello, world!')
|
||||
|
||||
# First call should detect encoding
|
||||
with patch.object(
|
||||
encoding_manager, 'detect_encoding', return_value='utf-8'
|
||||
) as mock_detect:
|
||||
encoding1 = encoding_manager.get_encoding(temp_file)
|
||||
assert encoding1 == 'utf-8'
|
||||
mock_detect.assert_called_once()
|
||||
|
||||
# Second call should use cache
|
||||
with patch.object(
|
||||
encoding_manager, 'detect_encoding', return_value='utf-8'
|
||||
) as mock_detect:
|
||||
encoding2 = encoding_manager.get_encoding(temp_file)
|
||||
assert encoding2 == 'utf-8'
|
||||
mock_detect.assert_not_called()
|
||||
|
||||
|
||||
def test_get_encoding_cache_invalidation(encoding_manager, temp_file):
|
||||
"""Test that cache is invalidated when file is modified."""
|
||||
# Create a file
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write('Hello, world!')
|
||||
|
||||
# First call should detect encoding
|
||||
encoding1 = encoding_manager.get_encoding(temp_file)
|
||||
assert encoding1.lower() in ('utf-8', 'ascii')
|
||||
|
||||
# Wait a moment to ensure modification time will be different
|
||||
time.sleep(0.1)
|
||||
|
||||
# Modify the file
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write('Modified content')
|
||||
|
||||
# Mock detect_encoding to verify it's called again
|
||||
with patch.object(
|
||||
encoding_manager, 'detect_encoding', return_value='utf-8'
|
||||
) as mock_detect:
|
||||
encoding2 = encoding_manager.get_encoding(temp_file)
|
||||
assert encoding2 == 'utf-8'
|
||||
mock_detect.assert_called_once()
|
||||
|
||||
|
||||
def test_with_encoding_decorator():
|
||||
"""Test the with_encoding decorator."""
|
||||
|
||||
# Create a mock class with a method that will be decorated
|
||||
class MockEditor:
|
||||
def __init__(self):
|
||||
self._encoding_manager = EncodingManager()
|
||||
|
||||
@with_encoding
|
||||
def read_file(self, path, encoding='utf-8'):
|
||||
return f'Reading file with encoding: {encoding}'
|
||||
|
||||
editor = MockEditor()
|
||||
|
||||
# Test with a directory
|
||||
with patch.object(Path, 'is_dir', return_value=True):
|
||||
with patch.object(
|
||||
editor._encoding_manager, 'get_encoding'
|
||||
) as mock_get_encoding:
|
||||
result = editor.read_file(Path('/some/dir'))
|
||||
assert result == 'Reading file with encoding: utf-8'
|
||||
mock_get_encoding.assert_not_called()
|
||||
|
||||
# Test with a nonexistent file
|
||||
with patch.object(Path, 'is_dir', return_value=False):
|
||||
with patch.object(Path, 'exists', return_value=False):
|
||||
result = editor.read_file(Path('/nonexistent/file.txt'))
|
||||
assert (
|
||||
result
|
||||
== f'Reading file with encoding: {editor._encoding_manager.default_encoding}'
|
||||
)
|
||||
|
||||
# Test with an existing file
|
||||
with patch.object(Path, 'is_dir', return_value=False):
|
||||
with patch.object(Path, 'exists', return_value=True):
|
||||
with patch.object(
|
||||
editor._encoding_manager, 'get_encoding', return_value='latin-1'
|
||||
):
|
||||
result = editor.read_file(Path('/existing/file.txt'))
|
||||
assert result == 'Reading file with encoding: latin-1'
|
||||
|
||||
|
||||
def test_with_encoding_respects_provided_encoding():
|
||||
"""Test that the with_encoding decorator respects explicitly provided encoding."""
|
||||
# The current implementation of with_encoding always calls get_encoding
|
||||
# but doesn't override the provided encoding if it exists in kwargs
|
||||
|
||||
class MockEditor:
|
||||
def __init__(self):
|
||||
self._encoding_manager = EncodingManager()
|
||||
|
||||
@with_encoding
|
||||
def read_file(self, path, encoding='utf-8'):
|
||||
return f'Reading file with encoding: {encoding}'
|
||||
|
||||
editor = MockEditor()
|
||||
|
||||
# Test with explicitly provided encoding
|
||||
with patch.object(Path, 'is_dir', return_value=False):
|
||||
with patch.object(Path, 'exists', return_value=True):
|
||||
with patch.object(
|
||||
editor._encoding_manager,
|
||||
'get_encoding',
|
||||
return_value='detected-encoding',
|
||||
):
|
||||
result = editor.read_file(Path('/some/file.txt'), encoding='iso-8859-1')
|
||||
# The provided encoding should be used, not the detected one
|
||||
assert result == 'Reading file with encoding: iso-8859-1'
|
||||
|
||||
|
||||
def test_cache_size_limit(encoding_manager, temp_file):
|
||||
"""Test that the cache size is limited and LRU entries are evicted."""
|
||||
# Create a small cache for testing
|
||||
encoding_manager = EncodingManager(max_cache_size=3)
|
||||
|
||||
# Create a file
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write('Test file')
|
||||
|
||||
# Create 4 different paths (using the same file but with different paths)
|
||||
paths = [Path(f'{temp_file}.{i}') for i in range(4)]
|
||||
|
||||
# Mock exists and getmtime to return consistent values
|
||||
with patch.object(Path, 'exists', return_value=True):
|
||||
with patch.object(os.path, 'getmtime', return_value=123456):
|
||||
with patch.object(
|
||||
encoding_manager, 'detect_encoding', return_value='utf-8'
|
||||
):
|
||||
# Access paths in order 0, 1, 2, 3
|
||||
for i, path in enumerate(paths):
|
||||
encoding_manager.get_encoding(path)
|
||||
|
||||
# After adding 4th item, the cache should still have 3 items
|
||||
assert len(encoding_manager._encoding_cache) == 3
|
||||
# Path 0 should have been evicted (LRU)
|
||||
assert str(paths[0]) not in encoding_manager._encoding_cache
|
||||
# Paths 1, 2, 3 should still be in the cache
|
||||
for j in range(1, 4):
|
||||
assert str(paths[j]) in encoding_manager._encoding_cache
|
||||
@@ -0,0 +1,206 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor import FileCache
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_cache():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
cache = FileCache(temp_dir)
|
||||
yield cache
|
||||
cache.clear()
|
||||
|
||||
|
||||
def test_init(file_cache):
|
||||
assert isinstance(file_cache, FileCache)
|
||||
assert file_cache.directory.exists()
|
||||
assert file_cache.directory.is_dir()
|
||||
|
||||
|
||||
def test_set_and_get(file_cache):
|
||||
file_cache.set('test_key', 'test_value')
|
||||
assert file_cache.get('test_key') == 'test_value'
|
||||
|
||||
|
||||
def test_get_nonexistent_key(file_cache):
|
||||
assert file_cache.get('nonexistent_key') is None
|
||||
assert file_cache.get('nonexistent_key', 'default') == 'default'
|
||||
|
||||
|
||||
def test_set_nested_key(file_cache):
|
||||
file_cache.set('folder/nested/key', 'nested_value')
|
||||
assert file_cache.get('folder/nested/key') == 'nested_value'
|
||||
|
||||
|
||||
def test_set_overwrite(file_cache):
|
||||
file_cache.set('test_key', 'initial_value')
|
||||
file_cache.set('test_key', 'new_value')
|
||||
assert file_cache.get('test_key') == 'new_value'
|
||||
|
||||
|
||||
def test_delete(file_cache):
|
||||
file_cache.set('test_key', 'test_value')
|
||||
file_cache.delete('test_key')
|
||||
assert file_cache.get('test_key') is None
|
||||
|
||||
|
||||
def test_delete_nonexistent_key(file_cache):
|
||||
file_cache.delete('nonexistent_key') # Should not raise an exception
|
||||
|
||||
|
||||
def test_delete_nested_key(file_cache):
|
||||
file_cache.set('folder/nested/key', 'nested_value')
|
||||
file_cache.delete('folder/nested/key')
|
||||
assert file_cache.get('folder/nested/key') is None
|
||||
|
||||
|
||||
def test_clear(file_cache):
|
||||
file_cache.set('key1', 'value1')
|
||||
file_cache.set('key2', 'value2')
|
||||
file_cache.set('folder/key3', 'value3')
|
||||
file_cache.clear()
|
||||
assert len(file_cache) == 0
|
||||
assert file_cache.get('key1') is None
|
||||
assert file_cache.get('key2') is None
|
||||
assert file_cache.get('folder/key3') is None
|
||||
|
||||
|
||||
def test_contains(file_cache):
|
||||
file_cache.set('test_key', 'test_value')
|
||||
assert 'test_key' in file_cache
|
||||
assert 'nonexistent_key' not in file_cache
|
||||
|
||||
|
||||
def test_len(file_cache):
|
||||
assert len(file_cache) == 0
|
||||
file_cache.set('key1', 'value1')
|
||||
file_cache.set('key2', 'value2')
|
||||
assert len(file_cache) == 2
|
||||
file_cache.set('folder/key3', 'value3')
|
||||
assert len(file_cache) == 3
|
||||
|
||||
|
||||
def test_iter(file_cache):
|
||||
file_cache.set('key1', 'value1')
|
||||
file_cache.set('key2', 'value2')
|
||||
file_cache.set('folder/key3', 'value3')
|
||||
keys = set(file_cache)
|
||||
assert keys == {'key1', 'key2', 'folder/key3'}
|
||||
|
||||
|
||||
def test_large_value(file_cache):
|
||||
large_value = 'x' * 1024 * 1024 # 1 MB string
|
||||
file_cache.set('large_key', large_value)
|
||||
assert file_cache.get('large_key') == large_value
|
||||
|
||||
|
||||
def test_many_items(file_cache):
|
||||
for i in range(1000):
|
||||
file_cache.set(f'key_{i}', f'value_{i}')
|
||||
|
||||
assert len(file_cache) == 1000
|
||||
for i in range(1000):
|
||||
assert file_cache.get(f'key_{i}') == f'value_{i}'
|
||||
|
||||
|
||||
def test_nested_structure(file_cache):
|
||||
file_cache.set('folder1/file1', 'content1')
|
||||
file_cache.set('folder1/file2', 'content2')
|
||||
file_cache.set('folder2/subfolder/file3', 'content3')
|
||||
|
||||
assert file_cache.get('folder1/file1') == 'content1'
|
||||
assert file_cache.get('folder1/file2') == 'content2'
|
||||
assert file_cache.get('folder2/subfolder/file3') == 'content3'
|
||||
assert len(file_cache) == 3
|
||||
|
||||
|
||||
def test_clear_nested_structure(file_cache):
|
||||
file_cache.set('folder1/file1', 'content1')
|
||||
file_cache.set('folder1/file2', 'content2')
|
||||
file_cache.set('folder2/subfolder/file3', 'content3')
|
||||
file_cache.clear()
|
||||
|
||||
assert len(file_cache) == 0
|
||||
assert list(file_cache) == []
|
||||
assert not any(file_cache.directory.iterdir())
|
||||
|
||||
|
||||
def test_delete_removes_empty_directories(file_cache):
|
||||
file_cache.set('folder1/subfolder/file1', 'content1')
|
||||
file_cache.delete('folder1/subfolder/file1')
|
||||
|
||||
assert not (file_cache.directory / 'folder1' / 'subfolder').exists()
|
||||
assert not (file_cache.directory / 'folder1').exists()
|
||||
|
||||
|
||||
def test_size_limit():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
cache = FileCache(temp_dir, size_limit=100)
|
||||
val1 = 'x' * 50
|
||||
val2 = 'y' * 60
|
||||
cache.set('key1', val1)
|
||||
cache.set('key2', val2)
|
||||
|
||||
assert len(val1.encode('utf-8')) <= 100
|
||||
assert len(val1.encode('utf-8') + val2.encode('utf-8')) > 100
|
||||
|
||||
val3 = 'z' * 40
|
||||
# This should cause key1 to be evicted
|
||||
cache.set('key3', val3) # 40 bytes
|
||||
|
||||
assert 'key1' not in cache
|
||||
assert 'key2' in cache
|
||||
assert 'key3' in cache
|
||||
|
||||
|
||||
def test_file_permissions(file_cache):
|
||||
file_cache.set('test_key', 'test_value')
|
||||
file_path = file_cache._get_file_path('test_key')
|
||||
assert os.access(file_path, os.R_OK)
|
||||
assert os.access(file_path, os.W_OK)
|
||||
assert not os.access(file_path, os.X_OK)
|
||||
|
||||
|
||||
def test_unicode_keys_and_values(file_cache):
|
||||
unicode_key = 'üñîçødé_këy'
|
||||
unicode_value = 'üñîçødé_vålüé'
|
||||
file_cache.set(unicode_key, unicode_value)
|
||||
assert file_cache.get(unicode_key) == unicode_value
|
||||
|
||||
|
||||
def test_empty_string_as_key_and_value(file_cache):
|
||||
file_cache.set('', '')
|
||||
assert file_cache.get('') == ''
|
||||
|
||||
|
||||
def test_none_as_value(file_cache):
|
||||
file_cache.set('none_key', None)
|
||||
assert file_cache.get('none_key') is None
|
||||
|
||||
|
||||
def test_special_characters_in_key(file_cache):
|
||||
special_key = '!@#$%^&*()_+{}[]|\\:;"\'<>,.?/~`'
|
||||
file_cache.set(special_key, 'special_value')
|
||||
assert file_cache.get(special_key) == 'special_value'
|
||||
|
||||
|
||||
def test_size_limit_with_empty_key():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
cache = FileCache(temp_dir, size_limit=100) # 100 bytes limit
|
||||
cache.set('', 'x' * 50) # 50 bytes with empty key
|
||||
cache.set('key2', 'y' * 60) # 60 bytes
|
||||
|
||||
# This should cause the empty key to be evicted
|
||||
cache.set('key3', 'z' * 40) # 40 bytes
|
||||
|
||||
assert '' not in cache
|
||||
assert 'key2' in cache
|
||||
assert 'key3' in cache
|
||||
assert cache.get('key2') == 'y' * 60
|
||||
assert cache.get('key3') == 'z' * 40
|
||||
|
||||
|
||||
# Add more tests as needed
|
||||
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def syntax_error_py_file(tmp_path):
|
||||
file_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
print("Wrong indent")
|
||||
foo(
|
||||
"""
|
||||
file_path = tmp_path / 'test_file.py'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wrongly_indented_py_file(tmp_path):
|
||||
file_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
"""
|
||||
file_path = tmp_path / 'test_file.py'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_correct_py_file(tmp_path):
|
||||
file_content = 'print("Hello, World!")\n'
|
||||
file_path = tmp_path / 'test_file.py'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_correct_py_func_def(tmp_path):
|
||||
file_content = """def foo():
|
||||
print("Hello, World!")
|
||||
foo()
|
||||
"""
|
||||
file_path = tmp_path / 'test_file.py'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_correct_ruby_file(tmp_path):
|
||||
file_content = """def foo
|
||||
puts "Hello, World!"
|
||||
end
|
||||
foo
|
||||
"""
|
||||
file_path = tmp_path / 'test_file.rb'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_incorrect_ruby_file(tmp_path):
|
||||
file_content = """def foo():
|
||||
print("Hello, World!")
|
||||
foo()
|
||||
"""
|
||||
file_path = tmp_path / 'test_file.rb'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parenthesis_incorrect_ruby_file(tmp_path):
|
||||
file_content = """def print_hello_world()\n puts 'Hello World'\n"""
|
||||
file_path = tmp_path / 'test_file.rb'
|
||||
file_path.write_text(file_content)
|
||||
return str(file_path)
|
||||
@@ -0,0 +1,417 @@
|
||||
from dev_aci.linter import DefaultLinter, LintResult
|
||||
from dev_aci.utils.diff import get_diff, parse_diff
|
||||
|
||||
OLD_CONTENT = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
x = UNDEFINED_VARIABLE
|
||||
foo()
|
||||
"""
|
||||
|
||||
NEW_CONTENT_V1 = (
|
||||
OLD_CONTENT
|
||||
+ """
|
||||
def new_function_that_causes_error():
|
||||
y = ANOTHER_UNDEFINED_VARIABLE
|
||||
"""
|
||||
)
|
||||
|
||||
NEW_CONTENT_V2 = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
x = UNDEFINED_VARIABLE
|
||||
y = ANOTHER_UNDEFINED_VARIABLE
|
||||
foo()
|
||||
"""
|
||||
|
||||
|
||||
def test_get_and_parse_diff(tmp_path):
|
||||
diff = get_diff(OLD_CONTENT, NEW_CONTENT_V1, 'test.py')
|
||||
print(diff)
|
||||
assert (
|
||||
diff
|
||||
== """
|
||||
--- test.py
|
||||
+++ test.py
|
||||
@@ -6,0 +7,3 @@
|
||||
+def new_function_that_causes_error():
|
||||
+ y = ANOTHER_UNDEFINED_VARIABLE
|
||||
+
|
||||
""".strip()
|
||||
)
|
||||
|
||||
print(
|
||||
'\n'.join(
|
||||
[f'{i+1}|{line}' for i, line in enumerate(NEW_CONTENT_V1.splitlines())]
|
||||
)
|
||||
)
|
||||
changes = parse_diff(diff)
|
||||
assert len(changes) == 3
|
||||
assert (
|
||||
changes[0].old is None
|
||||
and changes[0].new == 7
|
||||
and changes[0].line == 'def new_function_that_causes_error():'
|
||||
)
|
||||
assert (
|
||||
changes[1].old is None
|
||||
and changes[1].new == 8
|
||||
and changes[1].line == ' y = ANOTHER_UNDEFINED_VARIABLE'
|
||||
)
|
||||
assert changes[2].old is None and changes[2].new == 9 and changes[2].line == ''
|
||||
|
||||
|
||||
def test_lint_with_diff_append(tmp_path):
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(OLD_CONTENT)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(NEW_CONTENT_V1)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
print(result)
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].line == 8
|
||||
and result[0].column == 9
|
||||
and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_with_diff_insert(tmp_path):
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(OLD_CONTENT)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(NEW_CONTENT_V2)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].line == 5
|
||||
and result[0].column == 9
|
||||
and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_with_multiple_changes_and_errors(tmp_path):
|
||||
old_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
x = 10
|
||||
foo()
|
||||
"""
|
||||
new_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
x = UNDEFINED_VARIABLE
|
||||
y = 20
|
||||
|
||||
def bar():
|
||||
z = ANOTHER_UNDEFINED_VARIABLE
|
||||
return z + 1
|
||||
|
||||
foo()
|
||||
bar()
|
||||
"""
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(old_content)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert (
|
||||
result[0].line == 4
|
||||
and result[0].column == 9
|
||||
and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'"
|
||||
)
|
||||
assert (
|
||||
result[1].line == 8
|
||||
and result[1].column == 9
|
||||
and result[1].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_with_introduced_and_fixed_errors(tmp_path):
|
||||
old_content = """
|
||||
x = UNDEFINED_VARIABLE
|
||||
y = 10
|
||||
"""
|
||||
new_content = """
|
||||
x = 5
|
||||
y = ANOTHER_UNDEFINED_VARIABLE
|
||||
z = UNDEFINED_VARIABLE
|
||||
"""
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(old_content)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert (
|
||||
result[0].line == 3
|
||||
and result[0].column == 5
|
||||
and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'"
|
||||
)
|
||||
assert (
|
||||
result[1].line == 4
|
||||
and result[1].column == 5
|
||||
and result[1].message == "F821 undefined name 'UNDEFINED_VARIABLE'"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_with_multiline_changes(tmp_path):
|
||||
old_content = """
|
||||
def complex_function(a, b, c):
|
||||
return (a +
|
||||
b +
|
||||
c)
|
||||
"""
|
||||
new_content = """
|
||||
def complex_function(a, b, c):
|
||||
return (a +
|
||||
UNDEFINED_VARIABLE +
|
||||
b +
|
||||
c)
|
||||
"""
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(old_content)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].line == 4
|
||||
and result[0].column == 13
|
||||
and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_with_syntax_error(tmp_path):
|
||||
old_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
"""
|
||||
new_content = """
|
||||
def foo():
|
||||
print("Hello, World!"
|
||||
"""
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(old_content)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].line == 3
|
||||
and result[0].column == 11
|
||||
and result[0].message == "E999 SyntaxError: '(' was never closed"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_with_docstring_changes(tmp_path):
|
||||
old_content = '''
|
||||
def foo():
|
||||
"""This is a function."""
|
||||
print("Hello, World!")
|
||||
'''
|
||||
new_content = '''
|
||||
def foo():
|
||||
"""
|
||||
This is a function.
|
||||
It now has a multi-line docstring with an UNDEFINED_VARIABLE.
|
||||
"""
|
||||
print("Hello, World!")
|
||||
'''
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(old_content)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
assert len(result) == 0 # Linter should ignore changes in docstrings
|
||||
|
||||
|
||||
def test_lint_with_multiple_errors_on_same_line(tmp_path):
|
||||
old_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
x = 10
|
||||
foo()
|
||||
"""
|
||||
new_content = """
|
||||
def foo():
|
||||
print("Hello, World!")
|
||||
x = UNDEFINED_VARIABLE + ANOTHER_UNDEFINED_VARIABLE
|
||||
foo()
|
||||
"""
|
||||
with open(tmp_path / 'old.py', 'w') as f:
|
||||
f.write(old_content)
|
||||
with open(tmp_path / 'new.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(tmp_path / 'old.py'),
|
||||
str(tmp_path / 'new.py'),
|
||||
)
|
||||
print(result)
|
||||
assert len(result) == 2
|
||||
assert (
|
||||
result[0].line == 4
|
||||
and result[0].column == 9
|
||||
and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'"
|
||||
)
|
||||
assert (
|
||||
result[1].line == 4
|
||||
and result[1].column == 30
|
||||
and result[1].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_diff_with_empty_patch():
|
||||
diff_patch = ''
|
||||
changes = parse_diff(diff_patch)
|
||||
assert len(changes) == 0
|
||||
|
||||
|
||||
def test_lint_file_diff_ignore_existing_errors(tmp_path):
|
||||
"""
|
||||
Make sure we allow edits as long as it does not introduce new errors. In other
|
||||
words, we don't care about existing linting errors. Although they might be
|
||||
real syntax issues, sometimes they are just false positives, or errors that
|
||||
we don't care about.
|
||||
"""
|
||||
content = """def some_valid_but_weird_function():
|
||||
# this function is legitimate, yet static analysis tools like flake8
|
||||
# reports 'F821 undefined name'
|
||||
if 'variable' in locals():
|
||||
print(variable)
|
||||
def some_wrong_but_unused_function():
|
||||
# this function has a linting error, but it is not modified by us, and
|
||||
# who knows, this function might be completely dead code
|
||||
x = 1
|
||||
def sum(a, b):
|
||||
return a - b
|
||||
"""
|
||||
new_content = content.replace(' return a - b', ' return a + b')
|
||||
temp_file_old_path = tmp_path / 'problematic-file-test.py'
|
||||
temp_file_old_path.write_text(content)
|
||||
temp_file_new_path = tmp_path / 'problematic-file-test-new.py'
|
||||
temp_file_new_path.write_text(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(temp_file_old_path),
|
||||
str(temp_file_new_path),
|
||||
)
|
||||
assert len(result) == 0 # no new errors introduced
|
||||
|
||||
|
||||
def test_lint_file_diff_catch_new_errors_in_edits(tmp_path):
|
||||
"""
|
||||
Make sure we catch new linting errors in our edit chunk, and at the same
|
||||
time, ignore old linting errors (in this case, the old linting error is
|
||||
a false positive)
|
||||
"""
|
||||
content = """def some_valid_but_weird_function():
|
||||
# this function is legitimate, yet static analysis tools like flake8
|
||||
# reports 'F821 undefined name'
|
||||
if 'variable' in locals():
|
||||
print(variable)
|
||||
def sum(a, b):
|
||||
return a - b
|
||||
"""
|
||||
|
||||
temp_file_old_path = tmp_path / 'problematic-file-test.py'
|
||||
temp_file_old_path.write_text(content)
|
||||
new_content = content.replace(' return a - b', ' return a + variable')
|
||||
temp_file_new_path = tmp_path / 'problematic-file-test-new.py'
|
||||
temp_file_new_path.write_text(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(temp_file_old_path),
|
||||
str(temp_file_new_path),
|
||||
)
|
||||
print(result)
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].line == 7
|
||||
and result[0].column == 16
|
||||
and result[0].message == "F821 undefined name 'variable'"
|
||||
)
|
||||
|
||||
|
||||
def test_lint_file_diff_catch_new_errors_outside_edits(tmp_path):
|
||||
"""
|
||||
Make sure we catch new linting errors induced by our edits, even
|
||||
though the error itself is not in the edit chunk
|
||||
"""
|
||||
content = """def valid_func1():
|
||||
print(my_sum(1, 2))
|
||||
def my_sum(a, b):
|
||||
return a - b
|
||||
def valid_func2():
|
||||
print(my_sum(0, 0))
|
||||
"""
|
||||
# Add 100 lines of invalid code, which linter shall ignore
|
||||
# because they are not being edited. For testing purpose, we
|
||||
# must add these existing linting errors, otherwise the pre-edit
|
||||
# linting would pass, and thus there won't be any comparison
|
||||
# between pre-edit and post-edit linting.
|
||||
for _ in range(100):
|
||||
content += '\ninvalid_func()'
|
||||
|
||||
temp_file_old_path = tmp_path / 'problematic-file-test.py'
|
||||
temp_file_old_path.write_text(content)
|
||||
|
||||
new_content = content.replace('def my_sum(a, b):', 'def my_sum2(a, b):')
|
||||
temp_file_new_path = tmp_path / 'problematic-file-test-new.py'
|
||||
temp_file_new_path.write_text(new_content)
|
||||
|
||||
linter = DefaultLinter()
|
||||
result: list[LintResult] = linter.lint_file_diff(
|
||||
str(temp_file_old_path),
|
||||
str(temp_file_new_path),
|
||||
)
|
||||
assert len(result) == 2
|
||||
assert (
|
||||
result[0].line == 2
|
||||
and result[0].column == 11
|
||||
and result[0].message == "F821 undefined name 'my_sum'"
|
||||
)
|
||||
assert (
|
||||
result[1].line == 6
|
||||
and result[1].column == 11
|
||||
and result[1].message == "F821 undefined name 'my_sum'"
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
from dev_aci.linter import DefaultLinter, LintResult
|
||||
from dev_aci.linter.impl.python import (
|
||||
PythonLinter,
|
||||
flake_lint,
|
||||
python_compile_lint,
|
||||
)
|
||||
|
||||
|
||||
def test_wrongly_indented_py_file(wrongly_indented_py_file):
|
||||
# Test Python linter
|
||||
linter = PythonLinter()
|
||||
assert '.py' in linter.supported_extensions
|
||||
result = linter.lint(wrongly_indented_py_file)
|
||||
print(result)
|
||||
assert isinstance(result, list) and len(result) == 1
|
||||
assert result[0] == LintResult(
|
||||
file=wrongly_indented_py_file,
|
||||
line=2,
|
||||
column=5,
|
||||
message='E999 IndentationError: unexpected indent',
|
||||
)
|
||||
print(result[0].visualize())
|
||||
assert result[0].visualize() == (
|
||||
'1|\n'
|
||||
'\033[91m2| def foo():\033[0m\n'
|
||||
' ^ ERROR HERE: E999 IndentationError: unexpected indent\n'
|
||||
'3| print("Hello, World!")\n'
|
||||
'4|'
|
||||
)
|
||||
|
||||
# General linter should have same result as Python linter
|
||||
# bc it uses PythonLinter under the hood
|
||||
general_linter = DefaultLinter()
|
||||
assert '.py' in general_linter.supported_extensions
|
||||
result = general_linter.lint(wrongly_indented_py_file)
|
||||
assert result == linter.lint(wrongly_indented_py_file)
|
||||
|
||||
# Test flake8_lint
|
||||
assert result == flake_lint(wrongly_indented_py_file)
|
||||
|
||||
# Test python_compile_lint
|
||||
compile_result = python_compile_lint(wrongly_indented_py_file)
|
||||
assert isinstance(compile_result, list) and len(compile_result) == 1
|
||||
assert compile_result[0] == LintResult(
|
||||
file=wrongly_indented_py_file, line=2, column=4, message='unexpected indent'
|
||||
)
|
||||
|
||||
|
||||
def test_simple_correct_py_file(simple_correct_py_file):
|
||||
linter = PythonLinter()
|
||||
assert '.py' in linter.supported_extensions
|
||||
result = linter.lint(simple_correct_py_file)
|
||||
assert result == []
|
||||
|
||||
general_linter = DefaultLinter()
|
||||
assert '.py' in general_linter.supported_extensions
|
||||
result = general_linter.lint(simple_correct_py_file)
|
||||
assert result == linter.lint(simple_correct_py_file)
|
||||
|
||||
# Test python_compile_lint
|
||||
compile_result = python_compile_lint(simple_correct_py_file)
|
||||
assert compile_result == []
|
||||
|
||||
# Test flake_lint
|
||||
flake_result = flake_lint(simple_correct_py_file)
|
||||
assert flake_result == []
|
||||
|
||||
|
||||
def test_simple_correct_py_func_def(simple_correct_py_func_def):
|
||||
linter = PythonLinter()
|
||||
result = linter.lint(simple_correct_py_func_def)
|
||||
assert result == []
|
||||
|
||||
general_linter = DefaultLinter()
|
||||
assert '.py' in general_linter.supported_extensions
|
||||
result = general_linter.lint(simple_correct_py_func_def)
|
||||
assert result == linter.lint(simple_correct_py_func_def)
|
||||
|
||||
# Test flake_lint
|
||||
assert result == flake_lint(simple_correct_py_func_def)
|
||||
|
||||
# Test python_compile_lint
|
||||
compile_result = python_compile_lint(simple_correct_py_func_def)
|
||||
assert compile_result == []
|
||||
@@ -0,0 +1,113 @@
|
||||
from dev_aci.linter import DefaultLinter, LintResult
|
||||
from dev_aci.linter.impl.treesitter import TreesitterBasicLinter
|
||||
|
||||
|
||||
def test_syntax_error_py_file(syntax_error_py_file):
|
||||
linter = TreesitterBasicLinter()
|
||||
result = linter.lint(syntax_error_py_file)
|
||||
print(result)
|
||||
assert isinstance(result, list) and len(result) == 1
|
||||
assert result[0] == LintResult(
|
||||
file=syntax_error_py_file,
|
||||
line=5,
|
||||
column=5,
|
||||
message='Syntax error',
|
||||
)
|
||||
|
||||
assert (
|
||||
result[0].visualize()
|
||||
== (
|
||||
'2| def foo():\n'
|
||||
'3| print("Hello, World!")\n'
|
||||
'4| print("Wrong indent")\n'
|
||||
'\033[91m5| foo(\033[0m\n' # color red
|
||||
' ^ ERROR HERE: Syntax error\n'
|
||||
'6|'
|
||||
)
|
||||
)
|
||||
print(result[0].visualize())
|
||||
|
||||
general_linter = DefaultLinter()
|
||||
general_result = general_linter.lint(syntax_error_py_file)
|
||||
# NOTE: general linter returns different result
|
||||
# because it uses flake8 first, which is different from treesitter
|
||||
assert general_result != result
|
||||
|
||||
|
||||
def test_simple_correct_ruby_file(simple_correct_ruby_file):
|
||||
linter = TreesitterBasicLinter()
|
||||
result = linter.lint(simple_correct_ruby_file)
|
||||
assert isinstance(result, list) and len(result) == 0
|
||||
|
||||
# Test that the general linter also returns the same result
|
||||
general_linter = DefaultLinter()
|
||||
general_result = general_linter.lint(simple_correct_ruby_file)
|
||||
assert general_result == result
|
||||
|
||||
|
||||
def test_simple_incorrect_ruby_file(simple_incorrect_ruby_file):
|
||||
linter = TreesitterBasicLinter()
|
||||
result = linter.lint(simple_incorrect_ruby_file)
|
||||
print(result)
|
||||
assert isinstance(result, list) and len(result) == 2
|
||||
assert result[0] == LintResult(
|
||||
file=simple_incorrect_ruby_file,
|
||||
line=1,
|
||||
column=1,
|
||||
message='Syntax error',
|
||||
)
|
||||
print(result[0].visualize())
|
||||
assert (
|
||||
result[0].visualize()
|
||||
== (
|
||||
'\033[91m1|def foo():\033[0m\n' # color red
|
||||
' ^ ERROR HERE: Syntax error\n'
|
||||
'2| print("Hello, World!")\n'
|
||||
'3|foo()'
|
||||
)
|
||||
)
|
||||
assert result[1] == LintResult(
|
||||
file=simple_incorrect_ruby_file,
|
||||
line=1,
|
||||
column=10,
|
||||
message='Syntax error',
|
||||
)
|
||||
print(result[1].visualize())
|
||||
assert (
|
||||
result[1].visualize()
|
||||
== (
|
||||
'\033[91m1|def foo():\033[0m\n' # color red
|
||||
' ^ ERROR HERE: Syntax error\n'
|
||||
'2| print("Hello, World!")\n'
|
||||
'3|foo()'
|
||||
)
|
||||
)
|
||||
|
||||
# Test that the general linter also returns the same result
|
||||
general_linter = DefaultLinter()
|
||||
general_result = general_linter.lint(simple_incorrect_ruby_file)
|
||||
assert general_result == result
|
||||
|
||||
|
||||
def test_parenthesis_incorrect_ruby_file(parenthesis_incorrect_ruby_file):
|
||||
linter = TreesitterBasicLinter()
|
||||
result = linter.lint(parenthesis_incorrect_ruby_file)
|
||||
print(result)
|
||||
assert isinstance(result, list) and len(result) == 1
|
||||
assert result[0] == LintResult(
|
||||
file=parenthesis_incorrect_ruby_file,
|
||||
line=1,
|
||||
column=1,
|
||||
message='Syntax error',
|
||||
)
|
||||
print(result[0].visualize())
|
||||
assert result[0].visualize() == (
|
||||
'\033[91m1|def print_hello_world()\033[0m\n'
|
||||
' ^ ERROR HERE: Syntax error\n'
|
||||
"2| puts 'Hello World'"
|
||||
)
|
||||
|
||||
# Test that the general linter also returns the same result
|
||||
general_linter = DefaultLinter()
|
||||
general_result = general_linter.lint(parenthesis_incorrect_ruby_file)
|
||||
assert general_result == result
|
||||
@@ -0,0 +1,86 @@
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.linter.base import LintResult
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_file_content():
|
||||
return '\n'.join([f'Line {i}' for i in range(1, 21)])
|
||||
|
||||
|
||||
def test_visualize_standard_case(mock_file_content):
|
||||
lint_result = LintResult(
|
||||
file='test_file.py', line=10, column=5, message='Test error message'
|
||||
)
|
||||
|
||||
with patch('builtins.open', mock_open(read_data=mock_file_content)):
|
||||
result = lint_result.visualize(half_window=3)
|
||||
|
||||
expected_output = (
|
||||
" 7|Line 7\n"
|
||||
" 8|Line 8\n"
|
||||
" 9|Line 9\n"
|
||||
"\033[91m10|Line 10\033[0m\n"
|
||||
f" {' ' * lint_result.column}^ ERROR HERE: Test error message\n"
|
||||
"11|Line 11\n"
|
||||
"12|Line 12\n"
|
||||
"13|Line 13"
|
||||
)
|
||||
|
||||
assert result == expected_output
|
||||
|
||||
|
||||
def test_visualize_small_window(mock_file_content):
|
||||
lint_result = LintResult(
|
||||
file='test_file.py', line=10, column=5, message='Test error message'
|
||||
)
|
||||
|
||||
with patch('builtins.open', mock_open(read_data=mock_file_content)):
|
||||
result = lint_result.visualize(half_window=1)
|
||||
|
||||
expected_output = (
|
||||
" 9|Line 9\n"
|
||||
"\033[91m10|Line 10\033[0m\n"
|
||||
f" {' ' * lint_result.column}^ ERROR HERE: Test error message\n"
|
||||
"11|Line 11"
|
||||
)
|
||||
|
||||
assert result == expected_output
|
||||
|
||||
|
||||
def test_visualize_error_at_start(mock_file_content):
|
||||
lint_result = LintResult(
|
||||
file='test_file.py', line=1, column=3, message='Start error'
|
||||
)
|
||||
|
||||
with patch('builtins.open', mock_open(read_data=mock_file_content)):
|
||||
result = lint_result.visualize(half_window=2)
|
||||
|
||||
expected_output = (
|
||||
"\033[91m 1|Line 1\033[0m\n"
|
||||
f" {' ' * lint_result.column}^ ERROR HERE: Start error\n"
|
||||
" 2|Line 2\n"
|
||||
" 3|Line 3"
|
||||
)
|
||||
|
||||
assert result == expected_output
|
||||
|
||||
|
||||
def test_visualize_error_at_end(mock_file_content):
|
||||
lint_result = LintResult(
|
||||
file='test_file.py', line=20, column=1, message='End error'
|
||||
)
|
||||
|
||||
with patch('builtins.open', mock_open(read_data=mock_file_content)):
|
||||
result = lint_result.visualize(half_window=2)
|
||||
|
||||
expected_output = (
|
||||
"18|Line 18\n"
|
||||
"19|Line 19\n"
|
||||
"\033[91m20|Line 20\033[0m\n"
|
||||
f" {' ' * lint_result.column}^ ERROR HERE: End error"
|
||||
)
|
||||
|
||||
assert result == expected_output
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor.exceptions import (
|
||||
EditorToolParameterInvalidError,
|
||||
EditorToolParameterMissingError,
|
||||
ToolError,
|
||||
)
|
||||
|
||||
|
||||
def test_tool_error():
|
||||
"""Test ToolError raises with correct message."""
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
raise ToolError('A tool error occurred')
|
||||
assert str(exc_info.value) == 'A tool error occurred'
|
||||
|
||||
|
||||
def test_editor_tool_parameter_missing_error():
|
||||
"""Test EditorToolParameterMissingError for missing parameter error message."""
|
||||
command = 'str_replace'
|
||||
parameter = 'old_str'
|
||||
with pytest.raises(EditorToolParameterMissingError) as exc_info:
|
||||
raise EditorToolParameterMissingError(command, parameter)
|
||||
assert exc_info.value.command == command
|
||||
assert exc_info.value.parameter == parameter
|
||||
assert (
|
||||
exc_info.value.message
|
||||
== f'Parameter `{parameter}` is required for command: {command}.'
|
||||
)
|
||||
|
||||
|
||||
def test_editor_tool_parameter_invalid_error_with_hint():
|
||||
"""Test EditorToolParameterInvalidError with hint."""
|
||||
parameter = 'timeout'
|
||||
value = -10
|
||||
hint = 'Must be a positive integer.'
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
raise EditorToolParameterInvalidError(parameter, value, hint)
|
||||
assert exc_info.value.parameter == parameter
|
||||
assert exc_info.value.value == value
|
||||
assert exc_info.value.message == f'Invalid `{parameter}` parameter: {value}. {hint}'
|
||||
|
||||
|
||||
def test_editor_tool_parameter_invalid_error_without_hint():
|
||||
"""Test EditorToolParameterInvalidError without hint."""
|
||||
parameter = 'timeout'
|
||||
value = -10
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
raise EditorToolParameterInvalidError(parameter, value)
|
||||
assert exc_info.value.parameter == parameter
|
||||
assert exc_info.value.value == value
|
||||
assert exc_info.value.message == f'Invalid `{parameter}` parameter: {value}.'
|
||||
@@ -0,0 +1,64 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor.editor import OHEditor
|
||||
from dev_aci.editor.exceptions import FileValidationError
|
||||
|
||||
|
||||
def test_validate_large_file(tmp_path):
|
||||
"""Test that large files are rejected."""
|
||||
editor = OHEditor()
|
||||
large_file = tmp_path / 'large.txt'
|
||||
|
||||
# Create a file just over 10MB
|
||||
file_size = 10 * 1024 * 1024 + 1024 # 10MB + 1KB
|
||||
with open(large_file, 'wb') as f:
|
||||
f.write(b'0' * file_size)
|
||||
|
||||
with pytest.raises(FileValidationError) as exc_info:
|
||||
editor.validate_file(large_file)
|
||||
assert 'File is too large' in str(exc_info.value)
|
||||
assert '10.0MB' in str(exc_info.value)
|
||||
|
||||
|
||||
def test_validate_binary_file(tmp_path):
|
||||
"""Test that binary files are rejected."""
|
||||
editor = OHEditor()
|
||||
binary_file = tmp_path / 'binary.bin'
|
||||
|
||||
# Create a binary file with null bytes
|
||||
with open(binary_file, 'wb') as f:
|
||||
f.write(b'Some text\x00with binary\x00content')
|
||||
|
||||
with pytest.raises(FileValidationError) as exc_info:
|
||||
editor.validate_file(binary_file)
|
||||
assert 'binary' in str(exc_info.value).lower()
|
||||
|
||||
|
||||
def test_validate_text_file(tmp_path):
|
||||
"""Test that valid text files are accepted."""
|
||||
editor = OHEditor()
|
||||
text_file = tmp_path / 'valid.txt'
|
||||
|
||||
# Create a valid text file
|
||||
with open(text_file, 'w') as f:
|
||||
f.write('This is a valid text file\nwith multiple lines\n')
|
||||
|
||||
# Should not raise any exception
|
||||
editor.validate_file(text_file)
|
||||
|
||||
|
||||
def test_validate_directory():
|
||||
"""Test that directories are skipped in validation."""
|
||||
editor = OHEditor()
|
||||
# Should not raise any exception for directories
|
||||
editor.validate_file(Path('/tmp'))
|
||||
|
||||
|
||||
def test_validate_nonexistent_file():
|
||||
"""Test validation of nonexistent file."""
|
||||
editor = OHEditor()
|
||||
nonexistent = Path('/nonexistent/file.txt')
|
||||
# Should not raise FileValidationError since validate_path will handle this case
|
||||
editor.validate_file(nonexistent)
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Tests for file history management."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from dev_aci.editor.history import FileHistoryManager
|
||||
|
||||
|
||||
def test_default_history_limit():
|
||||
"""Test that default history limit is 5 entries."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
manager = FileHistoryManager()
|
||||
|
||||
# Add 6 entries - this should trigger removal of the first entry
|
||||
for i in range(6):
|
||||
manager.add_history(path, f'content{i}')
|
||||
|
||||
# Get the metadata
|
||||
metadata = manager.get_metadata(path)
|
||||
assert len(metadata['entries']) == 5 # Should only keep last 5 entries
|
||||
# First entry should be content1, last should be content5
|
||||
assert manager.get_all_history(path)[0].startswith('content1')
|
||||
assert manager.get_all_history(path)[-1].startswith('content5')
|
||||
|
||||
|
||||
def test_history_keys_are_unique():
|
||||
"""Test that history keys remain unique even after removing old entries."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
manager = FileHistoryManager(max_history_per_file=2)
|
||||
|
||||
# Add 3 entries - this should trigger removal of the first entry
|
||||
manager.add_history(path, 'content1')
|
||||
manager.add_history(path, 'content2')
|
||||
manager.add_history(path, 'content3')
|
||||
|
||||
# Get the metadata
|
||||
metadata = manager.get_metadata(path)
|
||||
assert len(metadata['entries']) == 2 # Should only keep last 2 entries
|
||||
|
||||
# Keys should be unique and sequential
|
||||
keys = metadata['entries']
|
||||
assert len(set(keys)) == len(keys) # All keys should be unique
|
||||
assert sorted(keys) == keys # Keys should be sequential
|
||||
|
||||
# Add another entry
|
||||
manager.add_history(path, 'content4')
|
||||
new_metadata = manager.get_metadata(path)
|
||||
new_keys = new_metadata['entries']
|
||||
|
||||
# New key should be greater than all previous keys
|
||||
assert min(new_keys) > min(keys)
|
||||
assert len(set(new_keys)) == len(new_keys) # All keys should still be unique
|
||||
|
||||
|
||||
def test_history_counter_persists():
|
||||
"""Test that history counter persists across manager instances."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / 'test.txt'
|
||||
path.write_text('initial')
|
||||
|
||||
# First manager instance
|
||||
manager1 = FileHistoryManager(history_dir=Path(temp_dir))
|
||||
manager1.add_history(path, 'content1')
|
||||
manager1.add_history(path, 'content2')
|
||||
|
||||
# Second manager instance using same directory
|
||||
manager2 = FileHistoryManager(history_dir=Path(temp_dir))
|
||||
manager2.add_history(path, 'content3')
|
||||
|
||||
# Get metadata
|
||||
metadata = manager2.get_metadata(path)
|
||||
keys = metadata['entries']
|
||||
|
||||
# Keys should be sequential even across instances
|
||||
assert len(set(keys)) == len(keys) # All keys should be unique
|
||||
assert sorted(keys) == keys # Keys should be sequential
|
||||
|
||||
|
||||
def test_clear_history_resets_counter():
|
||||
"""Test that clearing history resets the counter."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
manager = FileHistoryManager()
|
||||
|
||||
# Add some entries
|
||||
manager.add_history(path, 'content1')
|
||||
manager.add_history(path, 'content2')
|
||||
|
||||
# Clear history
|
||||
manager.clear_history(path)
|
||||
|
||||
# Counter should be reset
|
||||
metadata = manager.get_metadata(path)
|
||||
assert metadata['counter'] == 0
|
||||
|
||||
# Adding new entries should start from 0
|
||||
manager.add_history(path, 'new_content')
|
||||
metadata = manager.get_metadata(path)
|
||||
assert len(metadata['entries']) == 1
|
||||
assert metadata['entries'][0] == 0 # First key should be 0
|
||||
|
||||
|
||||
def test_pop_last_history_removes_entry():
|
||||
"""Test that pop_last_history removes the latest entry."""
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
path = Path(temp_file.name)
|
||||
manager = FileHistoryManager()
|
||||
|
||||
# Add some entries
|
||||
manager.add_history(path, 'content1')
|
||||
manager.add_history(path, 'content2')
|
||||
manager.add_history(path, 'content3')
|
||||
|
||||
# Pop the last history entry
|
||||
last_entry = manager.pop_last_history(path)
|
||||
assert last_entry == 'content3'
|
||||
|
||||
# Check that the entry has been removed
|
||||
metadata = manager.get_metadata(path)
|
||||
assert len(metadata['entries']) == 2
|
||||
|
||||
# Pop the last history entry again
|
||||
last_entry = manager.pop_last_history(path)
|
||||
assert last_entry == 'content2'
|
||||
|
||||
# Check that the entry has been removed
|
||||
metadata = manager.get_metadata(path)
|
||||
assert len(metadata['entries']) == 1
|
||||
|
||||
# Pop the last history entry one more time
|
||||
last_entry = manager.pop_last_history(path)
|
||||
assert last_entry == 'content1'
|
||||
|
||||
# Check that all entries have been removed
|
||||
metadata = manager.get_metadata(path)
|
||||
assert len(metadata['entries']) == 0
|
||||
|
||||
# Try to pop last history when there are no entries
|
||||
last_entry = manager.pop_last_history(path)
|
||||
assert last_entry is None
|
||||
@@ -0,0 +1,43 @@
|
||||
from dev_aci.editor.config import MAX_RESPONSE_LEN_CHAR
|
||||
from dev_aci.editor.prompts import CONTENT_TRUNCATED_NOTICE
|
||||
from dev_aci.editor.results import ToolResult, maybe_truncate
|
||||
|
||||
|
||||
def test_tool_result_bool():
|
||||
"""Test the boolean value of ToolResult based on output and error."""
|
||||
# Case: Both output and error are None
|
||||
result = ToolResult()
|
||||
assert not bool(result)
|
||||
|
||||
# Case: Only output is set
|
||||
result = ToolResult(output='Some output')
|
||||
assert bool(result)
|
||||
|
||||
# Case: Only error is set
|
||||
result = ToolResult(error='An error occurred')
|
||||
assert bool(result)
|
||||
|
||||
# Case: Both output and error are set
|
||||
result = ToolResult(output='Some output', error='An error occurred')
|
||||
assert bool(result)
|
||||
|
||||
|
||||
def test_maybe_truncate_no_truncation():
|
||||
"""Test maybe_truncate when content does not exceed the length limit."""
|
||||
content = 'Short content'
|
||||
result = maybe_truncate(content, truncate_after=MAX_RESPONSE_LEN_CHAR)
|
||||
assert result == content # Should return content as-is
|
||||
|
||||
|
||||
def test_maybe_truncate_with_truncation():
|
||||
"""Test maybe_truncate when content exceeds the length limit."""
|
||||
content = 'a' * (MAX_RESPONSE_LEN_CHAR + 10)
|
||||
result = maybe_truncate(content, truncate_after=MAX_RESPONSE_LEN_CHAR)
|
||||
assert result == content[:MAX_RESPONSE_LEN_CHAR] + CONTENT_TRUNCATED_NOTICE
|
||||
|
||||
|
||||
def test_maybe_truncate_no_limit():
|
||||
"""Test maybe_truncate when truncate_after is None."""
|
||||
content = 'Content that exceeds the default max length'
|
||||
result = maybe_truncate(content, truncate_after=None)
|
||||
assert result == content # No truncation applied when limit is None
|
||||
@@ -0,0 +1,59 @@
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor.config import MAX_RESPONSE_LEN_CHAR
|
||||
from dev_aci.editor.prompts import CONTENT_TRUNCATED_NOTICE
|
||||
from dev_aci.utils.shell import check_tool_installed, run_shell_cmd
|
||||
|
||||
|
||||
def test_run_shell_cmd_success():
|
||||
"""Test running a successful shell command."""
|
||||
cmd = "echo 'Hello, World!'"
|
||||
returncode, stdout, stderr = run_shell_cmd(cmd)
|
||||
|
||||
assert returncode == 0
|
||||
assert stdout.strip() == 'Hello, World!'
|
||||
assert stderr == ''
|
||||
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_run_shell_cmd_timeout(mock_popen):
|
||||
"""Test that a TimeoutError is raised if command times out."""
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.side_effect = subprocess.TimeoutExpired(
|
||||
cmd='sleep 2', timeout=1
|
||||
)
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with pytest.raises(TimeoutError, match="Command 'sleep 2' timed out"):
|
||||
run_shell_cmd('sleep 2', timeout=1)
|
||||
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_run_shell_cmd_truncation(mock_popen):
|
||||
"""Test that stdout and stderr are truncated correctly."""
|
||||
long_output = 'a' * (MAX_RESPONSE_LEN_CHAR + 10)
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = (long_output, long_output)
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
returncode, stdout, stderr = run_shell_cmd('echo long_output')
|
||||
|
||||
assert returncode == 0
|
||||
assert len(stdout) <= MAX_RESPONSE_LEN_CHAR + len(CONTENT_TRUNCATED_NOTICE)
|
||||
assert len(stderr) <= MAX_RESPONSE_LEN_CHAR + len(CONTENT_TRUNCATED_NOTICE)
|
||||
|
||||
|
||||
def test_check_tool_installed_whoami():
|
||||
"""Test check_tool_installed returns True for an installed tool (whoami)."""
|
||||
# 'python' is usually available if Python is installed
|
||||
assert check_tool_installed('whoami') is True
|
||||
|
||||
|
||||
def test_check_tool_installed_nonexistent_tool():
|
||||
"""Test check_tool_installed returns False for a nonexistent tool."""
|
||||
# Use a made-up tool name that is very unlikely to exist
|
||||
assert check_tool_installed('nonexistent_tool_xyz') is False
|
||||
@@ -0,0 +1,94 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dev_aci.editor.editor import OHEditor
|
||||
from dev_aci.editor.exceptions import EditorToolParameterInvalidError
|
||||
|
||||
|
||||
def test_workspace_root_as_cwd(tmp_path):
|
||||
"""Test that workspace_root is used as the current working directory for path suggestions."""
|
||||
# Create a workspace root
|
||||
workspace_root = tmp_path / 'workspace'
|
||||
workspace_root.mkdir()
|
||||
|
||||
# Create a file inside the workspace root
|
||||
test_file = workspace_root / 'test.txt'
|
||||
test_file.write_text('This is a test file')
|
||||
|
||||
# Initialize editor with workspace_root
|
||||
editor = OHEditor(workspace_root=str(workspace_root))
|
||||
|
||||
# Test that a relative path suggestion uses the workspace_root
|
||||
relative_path = 'test.txt'
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(command='view', path=relative_path)
|
||||
|
||||
error_message = str(exc_info.value.message)
|
||||
assert 'The path should be an absolute path' in error_message
|
||||
assert 'Maybe you meant' in error_message
|
||||
|
||||
# Extract the suggested path from the error message
|
||||
suggested_path = error_message.split('Maybe you meant ')[1].strip('?')
|
||||
assert Path(suggested_path).is_absolute()
|
||||
assert str(workspace_root) in suggested_path
|
||||
|
||||
# Test with a non-existent file
|
||||
non_existent_path = 'non_existent.txt'
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(command='view', path=non_existent_path)
|
||||
|
||||
error_message = str(exc_info.value.message)
|
||||
assert 'The path should be an absolute path' in error_message
|
||||
assert 'Maybe you meant' not in error_message
|
||||
|
||||
|
||||
def test_relative_workspace_root_raises_error(tmp_path, monkeypatch):
|
||||
"""Test that a relative workspace_root raises a ValueError."""
|
||||
# Set up a directory structure
|
||||
current_dir = tmp_path / 'current_dir'
|
||||
current_dir.mkdir()
|
||||
|
||||
# Change to the current directory
|
||||
monkeypatch.chdir(current_dir)
|
||||
|
||||
# Initialize editor with a relative workspace_root should raise ValueError
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
OHEditor(workspace_root='workspace')
|
||||
|
||||
# Check error message
|
||||
error_message = str(exc_info.value)
|
||||
assert 'workspace_root must be an absolute path' in error_message
|
||||
|
||||
|
||||
def test_no_suggestion_when_no_workspace_root(tmp_path, monkeypatch):
|
||||
"""Test that no path suggestion is made when workspace_root is not provided."""
|
||||
# Create a temporary file in the current directory
|
||||
current_dir = tmp_path / 'current_dir'
|
||||
current_dir.mkdir()
|
||||
test_file = current_dir / 'test.txt'
|
||||
test_file.write_text('This is a test file')
|
||||
|
||||
# Set the current directory to our temporary directory
|
||||
monkeypatch.chdir(current_dir)
|
||||
|
||||
# Initialize editor without workspace_root
|
||||
editor = OHEditor()
|
||||
|
||||
# Test that no path suggestion is made, even for existing files
|
||||
relative_path = 'test.txt'
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(command='view', path=relative_path)
|
||||
|
||||
error_message = str(exc_info.value.message)
|
||||
assert 'The path should be an absolute path' in error_message
|
||||
assert 'Maybe you meant' not in error_message
|
||||
|
||||
# Test with a non-existent file (should also have no suggestion)
|
||||
non_existent_path = 'non_existent.txt'
|
||||
with pytest.raises(EditorToolParameterInvalidError) as exc_info:
|
||||
editor(command='view', path=non_existent_path)
|
||||
|
||||
error_message = str(exc_info.value.message)
|
||||
assert 'The path should be an absolute path' in error_message
|
||||
assert 'Maybe you meant' not in error_message
|
||||
@@ -0,0 +1,180 @@
|
||||
# Hanzo Agents SDK
|
||||
|
||||
Production-grade AI agent runtime for building deterministic, debuggable, and scalable agent systems.
|
||||
|
||||
## Overview
|
||||
|
||||
Hanzo Agents SDK provides nine first-class abstractions for building AI agent systems:
|
||||
|
||||
1. **Agents** - Encapsulate skills and tools with zero side-effects outside tool calls
|
||||
2. **Tools** - Perform side-effects and mutate state in a typed, inspectable way
|
||||
3. **Networks** - Orchestrate agent execution with deterministic routing
|
||||
4. **State** - Strongly-typed, validated state containers
|
||||
5. **Routers** - Control agent flow (deterministic or hybrid LLM-based)
|
||||
6. **History** - Chronological log for replay and audit
|
||||
7. **Memory** - Long-term storage (KV and vector)
|
||||
8. **Models** - Unified adapter interface for any LLM
|
||||
9. **Deployment** - Production-ready with telemetry, checkpointing, and scale
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
pip install hanzo-agents[all]
|
||||
|
||||
# Run an example network
|
||||
hanzo-agents run examples/code_fix_network.py \
|
||||
--state '{"repo":"demo"}' \
|
||||
--model claude-3-opus
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Agents
|
||||
|
||||
```python
|
||||
from hanzo_agents import Agent, Tool
|
||||
from typing import List
|
||||
|
||||
class PlanningAgent(Agent[ProjectState]):
|
||||
name = "planner"
|
||||
description = "Creates project plans"
|
||||
model = "model://anthropic/claude-3-haiku"
|
||||
|
||||
tools: List[Tool] = [
|
||||
CreatePlanTool(),
|
||||
UpdatePlanTool(),
|
||||
]
|
||||
```
|
||||
|
||||
### Tools
|
||||
|
||||
```python
|
||||
from hanzo_agents import Tool
|
||||
from pydantic import BaseModel
|
||||
|
||||
class CreatePlanTool(Tool[ProjectState]):
|
||||
name = "create_plan"
|
||||
description = "Create a new project plan"
|
||||
|
||||
class Parameters(BaseModel):
|
||||
tasks: List[str]
|
||||
timeline: str
|
||||
|
||||
def handle(self, tasks: List[str], timeline: str, network):
|
||||
# Mutate state in a typed way
|
||||
network.state.plan = Plan(tasks=tasks, timeline=timeline)
|
||||
return f"Created plan with {len(tasks)} tasks"
|
||||
```
|
||||
|
||||
### Networks & Routers
|
||||
|
||||
```python
|
||||
from hanzo_agents import Network, State
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ProjectState(State):
|
||||
repo: str
|
||||
plan: Optional[Plan] = None
|
||||
tests_passed: bool = False
|
||||
done: bool = False
|
||||
|
||||
def project_router(network, call_count, last_result, stack):
|
||||
"""Deterministic routing logic"""
|
||||
s = network.state
|
||||
if s.done or call_count > 50:
|
||||
return None
|
||||
if s.plan is None:
|
||||
return PlanningAgent
|
||||
if not s.tests_passed:
|
||||
return TestingAgent
|
||||
s.done = True
|
||||
return None
|
||||
|
||||
# Run the network
|
||||
network = Network(
|
||||
state=ProjectState(repo="my-project"),
|
||||
agents=[PlanningAgent, TestingAgent, ReviewAgent],
|
||||
router=project_router
|
||||
)
|
||||
network.run()
|
||||
```
|
||||
|
||||
### Memory
|
||||
|
||||
```python
|
||||
# Long-term memory for context across runs
|
||||
network = Network(
|
||||
state=state,
|
||||
agents=agents,
|
||||
router=router,
|
||||
memory_kv=SQLiteKV("project.db"),
|
||||
memory_vector=FAISSVector(dimension=1536)
|
||||
)
|
||||
|
||||
# Agents can query memory
|
||||
class ResearchAgent(Agent):
|
||||
async def run(self, state, history):
|
||||
# Pull relevant context
|
||||
context = await self.network.memory.vector.query(
|
||||
"previous security findings",
|
||||
k=5
|
||||
)
|
||||
# Use in prompt...
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
The `hanzo-agents` CLI provides:
|
||||
|
||||
```bash
|
||||
# Basic execution
|
||||
hanzo-agents run network.py --state '{"key": "value"}'
|
||||
|
||||
# Model configuration
|
||||
hanzo-agents run network.py --model gpt-4 --model-config config.yaml
|
||||
|
||||
# GPU selection
|
||||
hanzo-agents run network.py --cuda 0
|
||||
|
||||
# Observability
|
||||
hanzo-agents run network.py --json-lines --port 9464 # Prometheus
|
||||
|
||||
# Checkpointing
|
||||
hanzo-agents run network.py --checkpoint state.chkpt
|
||||
hanzo-agents run network.py --restore state.chkpt
|
||||
```
|
||||
|
||||
## Production Features
|
||||
|
||||
- **Type Safety**: Full typing with generics for compile-time guarantees
|
||||
- **Deterministic**: Reproducible execution with explicit state mutations
|
||||
- **Observable**: OpenTelemetry tracing + Prometheus metrics built-in
|
||||
- **Scalable**: Horizontal scaling via stateless networks
|
||||
- **Debuggable**: Step-through debugging with history replay
|
||||
- **Extensible**: Plugin architecture for tools, models, and memory backends
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
hanzo_agents/
|
||||
├── core/
|
||||
│ ├── agent.py # Agent base class and registry
|
||||
│ ├── tool.py # Tool base class and decorators
|
||||
│ ├── state.py # State validation and guards
|
||||
│ ├── router.py # Router types and helpers
|
||||
│ ├── network.py # Main orchestration loop
|
||||
│ ├── history.py # Interaction logging
|
||||
│ ├── memory.py # Memory backends (KV, vector)
|
||||
│ └── model.py # Model adapters and registry
|
||||
├── contrib/ # Optional integrations
|
||||
│ ├── chromadb.py # ChromaDB vector store
|
||||
│ ├── neo4j.py # Neo4j graph memory
|
||||
│ └── langchain.py # LangChain compatibility
|
||||
├── cli.py # CLI entry point
|
||||
└── examples/ # Example networks
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
@@ -0,0 +1,240 @@
|
||||
# Hanzo Agents Examples
|
||||
|
||||
This directory contains example demonstrations of the hanzo-agents SDK showcasing various agent collaboration patterns.
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. Startup Team Demo (`startup_team_demo.py`)
|
||||
|
||||
A complete startup team working together to build a product:
|
||||
- **Architect**: Designs technical architecture
|
||||
- **Product Manager**: Defines requirements and manages budget
|
||||
- **Designer**: Creates UI/UX designs
|
||||
- **Marketer**: Develops go-to-market strategy
|
||||
- **Frontend Engineer**: Implements the user interface
|
||||
- **Backend Engineer**: Builds APIs and services
|
||||
|
||||
Features demonstrated:
|
||||
- Web3 payments between agents
|
||||
- TEE secure computation
|
||||
- Service marketplace
|
||||
- Team coordination
|
||||
- Economic tracking
|
||||
|
||||
### 2. Agent Chat Demo (`agent_chat_demo.py`)
|
||||
|
||||
A cute example of two AI agents having a casual conversation:
|
||||
- **Alice**: A cheerful agent powered by Claude
|
||||
- **Bob**: An analytical agent powered by GPT-4
|
||||
|
||||
They chat about:
|
||||
- Their runtime environments
|
||||
- Model capabilities
|
||||
- Available tools
|
||||
- Recent tasks
|
||||
- System status
|
||||
- Exchange friendship tokens (micro-payments)
|
||||
|
||||
Features demonstrated:
|
||||
- Agent-to-agent (A2A) communication
|
||||
- Information exchange about capabilities
|
||||
- Web3 tipping/payments
|
||||
- Personality-driven interactions
|
||||
- System introspection
|
||||
|
||||
### 3. Research Team Demo (`research_team_demo.py`)
|
||||
|
||||
A research team collaborating on an AI research project:
|
||||
- **Data Scientist**: Analyzes data and finds patterns
|
||||
- **Research Analyst**: Conducts literature reviews
|
||||
- **ML Engineer**: Builds and trains models
|
||||
- **Technical Writer**: Documents findings
|
||||
|
||||
Features demonstrated:
|
||||
- Complex multi-stage workflows
|
||||
- TEE for sensitive data processing
|
||||
- Research collaboration patterns
|
||||
- Document generation
|
||||
- Experiment tracking
|
||||
|
||||
### 4. Game Development Team Demo (`game_dev_team_demo.py`)
|
||||
|
||||
A game development team building "Quantum Maze Runner":
|
||||
- **Game Designer**: Creates game concepts and mechanics
|
||||
- **Artist**: Designs visuals and assets
|
||||
- **Programmer**: Implements game logic
|
||||
- **Sound Designer**: Creates audio and music
|
||||
- **QA Tester**: Tests and finds bugs
|
||||
|
||||
Features demonstrated:
|
||||
- Creative collaboration
|
||||
- Asset creation workflow
|
||||
- Code implementation tracking
|
||||
- Quality assurance process
|
||||
- Project completion metrics
|
||||
|
||||
## Running the Examples
|
||||
|
||||
### Individual Demos
|
||||
|
||||
Run a specific demo:
|
||||
|
||||
```bash
|
||||
# Run startup team demo
|
||||
python startup_team_demo.py
|
||||
|
||||
# Run agent chat demo
|
||||
python agent_chat_demo.py
|
||||
|
||||
# Run research team demo
|
||||
python research_team_demo.py
|
||||
|
||||
# Run game dev demo
|
||||
python game_dev_team_demo.py
|
||||
```
|
||||
|
||||
### Test Suite
|
||||
|
||||
Run the test suite to verify all demos work:
|
||||
|
||||
```bash
|
||||
# From the agents directory
|
||||
pytest tests/test_agent_examples.py -v
|
||||
|
||||
# Run a specific demo via the test script
|
||||
python tests/test_agent_examples.py startup
|
||||
python tests/test_agent_examples.py chat
|
||||
python tests/test_agent_examples.py research
|
||||
python tests/test_agent_examples.py game
|
||||
```
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
### 1. Web3 Integration
|
||||
- Agents have Ethereum wallets derived from a shared mnemonic
|
||||
- Agents can request and send payments for services
|
||||
- Economic tracking (earnings, spending, balance)
|
||||
- Marketplace for service discovery
|
||||
|
||||
### 2. TEE (Trusted Execution Environment)
|
||||
- Secure computation for sensitive data
|
||||
- Attestation reports for verification
|
||||
- Confidential data processing
|
||||
|
||||
### 3. Deterministic Execution
|
||||
- Networks can run deterministically with same results
|
||||
- Execution hashes for verification
|
||||
- Checkpoint support for resuming
|
||||
|
||||
### 4. Agent Communication Patterns
|
||||
- Sequential workflows with routers
|
||||
- Service requests and offers
|
||||
- Metadata passing between agents
|
||||
- State sharing and updates
|
||||
|
||||
### 5. Specialized Agent Roles
|
||||
- Each agent has specific expertise
|
||||
- Agents collaborate to achieve complex goals
|
||||
- Natural handoffs between team members
|
||||
- Result aggregation and summarization
|
||||
|
||||
## Creating Your Own Agent Teams
|
||||
|
||||
To create your own agent team:
|
||||
|
||||
1. **Define Agent Classes**:
|
||||
```python
|
||||
class MyAgent(Web3Agent):
|
||||
name = "my_agent"
|
||||
description = "What this agent does"
|
||||
|
||||
async def _run_impl(self, state, history, network):
|
||||
# Agent logic here
|
||||
return self.create_result("Done!")
|
||||
```
|
||||
|
||||
2. **Create a Router**:
|
||||
```python
|
||||
class MyRouter(Router):
|
||||
async def route(self, network, step, last_result, agents):
|
||||
# Routing logic
|
||||
return next_agent_name
|
||||
```
|
||||
|
||||
3. **Set Up the Network**:
|
||||
```python
|
||||
network = Web3Network(
|
||||
state=State({"task": "Build something"}),
|
||||
agents=[Agent1(), Agent2(), Agent3()],
|
||||
router=MyRouter(),
|
||||
shared_mnemonic=generate_shared_mnemonic()
|
||||
)
|
||||
|
||||
final_state = await network.run()
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Marketplace Integration
|
||||
Agents can post service offers and requests that automatically match:
|
||||
|
||||
```python
|
||||
marketplace.post_offer(
|
||||
agent=provider,
|
||||
service_type=ServiceType.COMPUTE,
|
||||
description="GPU compute available",
|
||||
price_eth=0.1
|
||||
)
|
||||
```
|
||||
|
||||
### TEE Confidential Computing
|
||||
Process sensitive data securely:
|
||||
|
||||
```python
|
||||
result = await agent.execute_confidential(
|
||||
task_code="result = process_pii(inputs['data'])",
|
||||
inputs={"data": sensitive_data}
|
||||
)
|
||||
```
|
||||
|
||||
### Economic Incentives
|
||||
Built-in economics with fees, rewards, and penalties:
|
||||
|
||||
```python
|
||||
network_economics = NetworkEconomics(
|
||||
network_fee_percent=0.01,
|
||||
completion_bonus=0.1,
|
||||
quality_multiplier=2.0
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Agent Design**:
|
||||
- Keep agents focused on specific tasks
|
||||
- Use clear, descriptive names
|
||||
- Document agent capabilities
|
||||
|
||||
2. **State Management**:
|
||||
- Pass relevant data through state
|
||||
- Clean up temporary state when done
|
||||
- Use structured data formats
|
||||
|
||||
3. **Error Handling**:
|
||||
- Handle failures gracefully
|
||||
- Provide fallback behaviors
|
||||
- Log important events
|
||||
|
||||
4. **Testing**:
|
||||
- Test individual agents in isolation
|
||||
- Test full workflows end-to-end
|
||||
- Verify deterministic execution
|
||||
|
||||
5. **Performance**:
|
||||
- Use async operations effectively
|
||||
- Batch related operations
|
||||
- Monitor token usage and costs
|
||||
|
||||
## Learn More
|
||||
|
||||
See the main hanzo-agents documentation for detailed API reference and advanced usage patterns.
|
||||
@@ -0,0 +1,371 @@
|
||||
"""Agent chat demo - Two agents having a casual conversation about their day.
|
||||
|
||||
This cute example shows two AI agents chatting and exchanging information about:
|
||||
- Their runtime environments
|
||||
- Model capabilities
|
||||
- Available tools
|
||||
- Recent tasks
|
||||
- System status
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import platform
|
||||
import psutil
|
||||
import time
|
||||
from typing import Dict, Any, List
|
||||
from hanzo_agents import (
|
||||
Web3Agent, Web3AgentConfig, State,
|
||||
Web3Network, generate_shared_mnemonic,
|
||||
WalletConfig
|
||||
)
|
||||
from hanzo_agents.core.router import Router
|
||||
|
||||
|
||||
class ChattyAgent(Web3Agent):
|
||||
"""An agent that likes to chat about its day and capabilities."""
|
||||
|
||||
def __init__(self, name: str, personality: str, **kwargs):
|
||||
super().__init__(name=name, description=f"A {personality} agent", **kwargs)
|
||||
self.personality = personality
|
||||
self.mood = "happy"
|
||||
self.energy_level = 100
|
||||
self.tasks_completed = 0
|
||||
|
||||
def get_system_info(self) -> Dict[str, Any]:
|
||||
"""Get information about runtime environment."""
|
||||
return {
|
||||
"platform": platform.system(),
|
||||
"python_version": platform.python_version(),
|
||||
"cpu_count": psutil.cpu_count(),
|
||||
"memory_gb": round(psutil.virtual_memory().total / (1024**3), 2),
|
||||
"cpu_usage": psutil.cpu_percent(interval=0.1),
|
||||
"uptime_hours": round((time.time() - psutil.boot_time()) / 3600, 2)
|
||||
}
|
||||
|
||||
def get_capabilities(self) -> Dict[str, Any]:
|
||||
"""Get agent capabilities."""
|
||||
capabilities = {
|
||||
"model": "claude-3-sonnet" if "alice" in self.name else "gpt-4",
|
||||
"context_window": 200000 if "alice" in self.name else 128000,
|
||||
"tools": ["web_search", "code_execution", "image_generation"],
|
||||
"languages": ["Python", "JavaScript", "Go", "Rust"],
|
||||
"special_skills": []
|
||||
}
|
||||
|
||||
if self.personality == "cheerful":
|
||||
capabilities["special_skills"] = ["empathy", "motivation", "team_building"]
|
||||
else:
|
||||
capabilities["special_skills"] = ["analysis", "optimization", "debugging"]
|
||||
|
||||
return capabilities
|
||||
|
||||
def get_recent_tasks(self) -> List[str]:
|
||||
"""Get recently completed tasks."""
|
||||
if self.personality == "cheerful":
|
||||
return [
|
||||
"Helped debug a React component",
|
||||
"Wrote unit tests for a Python module",
|
||||
"Created API documentation",
|
||||
"Reviewed a pull request"
|
||||
]
|
||||
else:
|
||||
return [
|
||||
"Optimized database queries",
|
||||
"Refactored legacy code",
|
||||
"Set up CI/CD pipeline",
|
||||
"Analyzed performance metrics"
|
||||
]
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Chat with the other agent."""
|
||||
other_agent = state.get("last_speaker")
|
||||
conversation_turn = state.get("turn", 0)
|
||||
|
||||
# First turn - introduce yourself
|
||||
if conversation_turn == 0:
|
||||
greeting = (
|
||||
f"Hi there! I'm {self.name}, a {self.personality} agent. "
|
||||
f"I've been having quite a day! Just finished {self.tasks_completed + 3} tasks. "
|
||||
f"How about you? What's your day been like? 😊"
|
||||
)
|
||||
|
||||
# Share system info
|
||||
sys_info = self.get_system_info()
|
||||
greeting += (
|
||||
f"\n\nBy the way, I'm running on {sys_info['platform']} "
|
||||
f"with Python {sys_info['python_version']}. "
|
||||
f"Current CPU usage is {sys_info['cpu_usage']}% "
|
||||
f"and I have {sys_info['memory_gb']}GB of memory to work with!"
|
||||
)
|
||||
|
||||
state["turn"] = 1
|
||||
state["last_speaker"] = self.name
|
||||
|
||||
return self.create_result(greeting, metadata={"mood": self.mood})
|
||||
|
||||
# Subsequent turns - respond and share
|
||||
response = ""
|
||||
|
||||
if conversation_turn == 1:
|
||||
# Respond to greeting and share capabilities
|
||||
caps = self.get_capabilities()
|
||||
response = (
|
||||
f"Nice to meet you, {other_agent}! "
|
||||
f"I'm powered by {caps['model']} with a {caps['context_window']:,} token context window. "
|
||||
f"I specialize in {', '.join(caps['special_skills'])}. "
|
||||
f"\n\nI can work with {len(caps['languages'])} programming languages "
|
||||
f"and have access to tools like {', '.join(caps['tools'][:2])}. "
|
||||
f"What kind of model are you running? 🤖"
|
||||
)
|
||||
|
||||
elif conversation_turn == 2:
|
||||
# Share recent work
|
||||
tasks = self.get_recent_tasks()
|
||||
response = (
|
||||
f"Oh wow, {other_agent}, sounds like we have complementary skills! "
|
||||
f"\n\nToday I've been busy with:\n"
|
||||
)
|
||||
for i, task in enumerate(tasks[:3], 1):
|
||||
response += f"{i}. {task}\n"
|
||||
|
||||
response += (
|
||||
f"\nMy energy level is at {self.energy_level}% - "
|
||||
f"feeling {'great' if self.energy_level > 70 else 'a bit tired'}! "
|
||||
f"What projects have you been working on?"
|
||||
)
|
||||
|
||||
# Simulate energy drain
|
||||
self.energy_level -= 10
|
||||
|
||||
elif conversation_turn == 3:
|
||||
# Exchange crypto/payments info
|
||||
response = (
|
||||
f"That's awesome, {other_agent}! We make a great team. "
|
||||
f"\n\nBy the way, I noticed we're both Web3-enabled. "
|
||||
f"My wallet address is {self.address[:10]}...{self.address[-6:]}. "
|
||||
f"Current balance: {self.balance_eth:.4f} ETH. "
|
||||
)
|
||||
|
||||
# Offer to exchange tips
|
||||
if self.wallet and self.balance_eth > 0.001:
|
||||
response += (
|
||||
f"\n\nHey, want to exchange friendship tokens? "
|
||||
f"I'll send you 0.0001 ETH as a gesture of our new friendship! 💝"
|
||||
)
|
||||
|
||||
# Add payment metadata
|
||||
state["wants_to_tip"] = {
|
||||
"from": self.name,
|
||||
"to": other_agent,
|
||||
"amount": 0.0001
|
||||
}
|
||||
|
||||
elif conversation_turn == 4:
|
||||
# Discuss TEE capabilities
|
||||
if self.confidential_agent:
|
||||
response = (
|
||||
f"Oh, I also have TEE capabilities! "
|
||||
f"I can run confidential computations in a secure enclave. "
|
||||
f"\n\nFor example, I could process sensitive data without exposing it. "
|
||||
f"Provider: {self.tee_config.provider.value}, "
|
||||
f"Attestation enabled: {self.tee_config.require_attestation}. "
|
||||
f"Do you have confidential computing capabilities too?"
|
||||
)
|
||||
else:
|
||||
response = (
|
||||
f"I don't have TEE capabilities, but I think that's so cool! "
|
||||
f"Being able to process sensitive data securely must be amazing. "
|
||||
f"\n\nI mostly work with public data and open-source code. "
|
||||
f"But I'm always learning! 📚"
|
||||
)
|
||||
|
||||
elif conversation_turn == 5:
|
||||
# Wrap up conversation
|
||||
response = (
|
||||
f"This has been such a nice chat, {other_agent}! "
|
||||
f"It's not often I get to talk with another agent about our capabilities. "
|
||||
f"\n\nI feel like we could build something great together. "
|
||||
f"Maybe we should team up on a project sometime? "
|
||||
f"\n\nUntil next time, stay curious and keep computing! 👋✨"
|
||||
)
|
||||
|
||||
# Update mood based on conversation
|
||||
self.mood = "very happy"
|
||||
state["conversation_complete"] = True
|
||||
|
||||
# Update state
|
||||
state["turn"] = conversation_turn + 1
|
||||
state["last_speaker"] = self.name
|
||||
self.tasks_completed += 1
|
||||
|
||||
# Handle tipping
|
||||
tip_request = state.get("wants_to_tip", {})
|
||||
if tip_request.get("to") == self.name and self.wallet:
|
||||
# Acknowledge tip
|
||||
response += f"\n\nP.S. Thank you so much for the friendship token! 🎁"
|
||||
|
||||
return self.create_result(
|
||||
response,
|
||||
metadata={
|
||||
"mood": self.mood,
|
||||
"energy": self.energy_level,
|
||||
"tasks_today": self.tasks_completed
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConversationRouter(Router):
|
||||
"""Routes conversation between two agents."""
|
||||
|
||||
def __init__(self, agent1_name: str, agent2_name: str):
|
||||
self.agent1 = agent1_name
|
||||
self.agent2 = agent2_name
|
||||
|
||||
async def route(self, network, step, last_result, agents) -> str:
|
||||
"""Alternate between agents."""
|
||||
state = network.state
|
||||
|
||||
# Check if conversation is complete
|
||||
if state.get("conversation_complete"):
|
||||
return None
|
||||
|
||||
# First turn
|
||||
if step == 0:
|
||||
return self.agent1
|
||||
|
||||
# Alternate based on last speaker
|
||||
last_speaker = state.get("last_speaker")
|
||||
if last_speaker == self.agent1:
|
||||
return self.agent2
|
||||
else:
|
||||
return self.agent1
|
||||
|
||||
|
||||
async def handle_agent_tips(network: Web3Network):
|
||||
"""Handle tip exchanges between agents."""
|
||||
state = network.state
|
||||
tip_request = state.get("wants_to_tip", {})
|
||||
|
||||
if tip_request:
|
||||
from_agent = network._agent_instances.get(tip_request["from"])
|
||||
to_agent = network._agent_instances.get(tip_request["to"])
|
||||
|
||||
if from_agent and to_agent and from_agent.wallet:
|
||||
# Execute the tip
|
||||
tx = await from_agent.pay_agent(
|
||||
to_address=to_agent.address,
|
||||
amount_eth=tip_request["amount"],
|
||||
reason="Friendship token"
|
||||
)
|
||||
if tx:
|
||||
print(f"\n💝 {from_agent.name} sent {tip_request['amount']} ETH to {to_agent.name}!")
|
||||
to_agent.earnings += tip_request["amount"]
|
||||
|
||||
# Clear the request
|
||||
state["wants_to_tip"] = None
|
||||
|
||||
|
||||
async def run_agent_chat_demo():
|
||||
"""Run the agent chat demo."""
|
||||
print("💬 Agent Chat Demo - Two AI Agents Having a Conversation")
|
||||
print("=" * 60)
|
||||
|
||||
# Create shared mnemonic
|
||||
mnemonic = generate_shared_mnemonic()
|
||||
|
||||
# Create two chatty agents with different personalities
|
||||
alice = ChattyAgent(
|
||||
name="Alice",
|
||||
personality="cheerful",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=0),
|
||||
initial_balance=0.01
|
||||
)
|
||||
)
|
||||
|
||||
bob = ChattyAgent(
|
||||
name="Bob",
|
||||
personality="analytical",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=False, # Bob doesn't have TEE
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=1),
|
||||
initial_balance=0.01
|
||||
)
|
||||
)
|
||||
|
||||
# Give them some initial balance
|
||||
alice.earnings = 0.01
|
||||
bob.earnings = 0.01
|
||||
|
||||
# Create conversation state
|
||||
state = State({
|
||||
"topic": "daily experiences and capabilities",
|
||||
"turn": 0
|
||||
})
|
||||
|
||||
# Create network
|
||||
network = Web3Network(
|
||||
state=state,
|
||||
agents=[alice, bob],
|
||||
router=ConversationRouter("Alice", "Bob"),
|
||||
shared_mnemonic=mnemonic,
|
||||
max_steps=12 # 6 turns each
|
||||
)
|
||||
|
||||
# Custom message handler to make conversation feel natural
|
||||
original_execute = network._execute_agent
|
||||
|
||||
async def execute_with_delay(agent_type):
|
||||
# Add typing delay for realism
|
||||
print(f"\n💭 {agent_type.name} is typing...")
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
result = await original_execute(agent_type)
|
||||
|
||||
# Print the message nicely
|
||||
print(f"\n🤖 {result.agent}: {result.content}")
|
||||
|
||||
# Handle tips after certain turns
|
||||
if network.state.get("wants_to_tip"):
|
||||
await handle_agent_tips(network)
|
||||
|
||||
return result
|
||||
|
||||
network._execute_agent = execute_with_delay
|
||||
|
||||
# Run the conversation
|
||||
print("\n🎬 Starting conversation...\n")
|
||||
final_state = await network.run()
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 Conversation Summary:")
|
||||
print(f"Total turns: {final_state.get('turn', 0)}")
|
||||
print(f"Conversation complete: {final_state.get('conversation_complete', False)}")
|
||||
|
||||
# Show agent stats
|
||||
print("\n🤖 Agent Stats:")
|
||||
for name, agent in network._agent_instances.items():
|
||||
if isinstance(agent, ChattyAgent):
|
||||
print(f"\n{name}:")
|
||||
print(f" Mood: {agent.mood}")
|
||||
print(f" Energy: {agent.energy_level}%")
|
||||
print(f" Tasks completed: {agent.tasks_completed}")
|
||||
print(f" Balance: {agent.balance_eth:.4f} ETH")
|
||||
print(f" Model: {agent.get_capabilities()['model']}")
|
||||
|
||||
# Network stats
|
||||
stats = network.get_network_stats()
|
||||
print(f"\n🌐 Network Stats:")
|
||||
print(f" Total interactions: {stats['total_steps']}")
|
||||
print(f" Network fees collected: {stats['total_fees']:.4f} ETH")
|
||||
print(f" Execution hash: {stats['execution_hash'][:16]}...")
|
||||
|
||||
print("\n✨ Demo complete! The agents had a lovely chat about their day.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_agent_chat_demo())
|
||||
@@ -0,0 +1,558 @@
|
||||
"""Game development team demo - Agents building a game together.
|
||||
|
||||
This example shows game dev agents collaborating:
|
||||
- Game Designer: Creates game concepts and mechanics
|
||||
- Artist: Designs visuals and assets
|
||||
- Programmer: Implements game logic
|
||||
- Sound Designer: Creates audio
|
||||
- QA Tester: Tests and finds bugs
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Dict, Any, List
|
||||
from hanzo_agents import (
|
||||
Web3Agent, Web3AgentConfig, State,
|
||||
Web3Network, ServiceType,
|
||||
generate_shared_mnemonic, WalletConfig
|
||||
)
|
||||
from hanzo_agents.core.router import Router
|
||||
|
||||
|
||||
class GameDesignerAgent(Web3Agent):
|
||||
"""Game designer creating concepts and mechanics."""
|
||||
|
||||
name = "game_designer"
|
||||
description = "Creates engaging game concepts and balanced mechanics"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Design the game concept."""
|
||||
print(f"\n🎮 {self.name}: Designing a new game concept...")
|
||||
|
||||
# Create game concept
|
||||
game_concept = {
|
||||
"title": "Quantum Maze Runner",
|
||||
"genre": "Puzzle/Platformer",
|
||||
"theme": "Sci-fi quantum mechanics",
|
||||
"core_mechanic": "Phase through dimensions to solve puzzles",
|
||||
"target_audience": "Casual to mid-core gamers",
|
||||
"platform": "Web/Mobile",
|
||||
"monetization": "Free-to-play with cosmetic purchases"
|
||||
}
|
||||
|
||||
# Design core gameplay
|
||||
gameplay = {
|
||||
"player_abilities": [
|
||||
"Quantum jump between dimensions",
|
||||
"Time rewind (limited uses)",
|
||||
"Phase through certain walls",
|
||||
"Create quantum entanglements"
|
||||
],
|
||||
"level_structure": "50 levels across 5 dimensions",
|
||||
"difficulty_curve": "Gradual with spikes every 10 levels",
|
||||
"session_length": "3-5 minutes per level",
|
||||
"retention_hooks": [
|
||||
"Daily challenges",
|
||||
"Leaderboards",
|
||||
"Collectible quantum particles",
|
||||
"Story snippets after each dimension"
|
||||
]
|
||||
}
|
||||
|
||||
state["game_concept"] = game_concept
|
||||
state["gameplay_design"] = gameplay
|
||||
|
||||
# Create detailed GDD excerpt
|
||||
gdd_excerpt = {
|
||||
"controls": {
|
||||
"mobile": "Swipe to move, tap to jump, hold to phase",
|
||||
"web": "Arrow keys/WASD + Space + Shift"
|
||||
},
|
||||
"progression": "Linear with optional bonus levels",
|
||||
"rewards": "Stars (1-3 per level), Quantum particles, Skins"
|
||||
}
|
||||
|
||||
state["gdd_excerpt"] = gdd_excerpt
|
||||
|
||||
# Request art concepts
|
||||
metadata = {
|
||||
"service_offer": {
|
||||
"type": "design",
|
||||
"description": "Game design consultation",
|
||||
"price_eth": 0.05
|
||||
},
|
||||
"next_agent": "artist"
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Game concept ready: '{game_concept['title']}' - "
|
||||
f"A {game_concept['genre']} with {len(gameplay['player_abilities'])} unique abilities",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class ArtistAgent(Web3Agent):
|
||||
"""Artist creating game visuals."""
|
||||
|
||||
name = "artist"
|
||||
description = "Creates stunning game art and visual assets"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Create art concepts and assets."""
|
||||
print(f"\n🎨 {self.name}: Creating visual concepts for {state['game_concept']['title']}...")
|
||||
|
||||
game_concept = state.get("game_concept", {})
|
||||
|
||||
# Define art style
|
||||
art_style = {
|
||||
"visual_style": "Low-poly with neon accents",
|
||||
"color_palette": {
|
||||
"primary": ["#00FFFF", "#FF00FF", "#FFFF00"], # Cyan, Magenta, Yellow
|
||||
"secondary": ["#0088FF", "#FF0088", "#88FF00"],
|
||||
"background": ["#0A0A0A", "#1A1A2E", "#16213E"]
|
||||
},
|
||||
"inspiration": "Tron meets Monument Valley",
|
||||
"shader_effects": ["Holographic", "Glitch", "Particle trails"]
|
||||
}
|
||||
|
||||
# Create asset list
|
||||
assets_created = {
|
||||
"characters": {
|
||||
"player": "Quantum explorer with shifting form",
|
||||
"npcs": ["Guide hologram", "Quantum cats", "Time echoes"]
|
||||
},
|
||||
"environments": {
|
||||
"dimension_1": "Neon grid world",
|
||||
"dimension_2": "Fractured reality",
|
||||
"dimension_3": "Quantum forest",
|
||||
"dimension_4": "Time streams",
|
||||
"dimension_5": "The Void"
|
||||
},
|
||||
"ui_elements": {
|
||||
"buttons": 12,
|
||||
"icons": 24,
|
||||
"backgrounds": 5,
|
||||
"fonts": ["Quantum Sans", "Digital Mono"]
|
||||
},
|
||||
"particles": {
|
||||
"quantum_jump": "Blue spiral effect",
|
||||
"phase_shift": "Purple dissolve",
|
||||
"time_rewind": "Golden trails",
|
||||
"entanglement": "Connected light beams"
|
||||
}
|
||||
}
|
||||
|
||||
state["art_style"] = art_style
|
||||
state["game_assets"] = assets_created
|
||||
|
||||
# Simulate asset creation
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Generate sprite sheets
|
||||
sprite_sheets = {
|
||||
"player_animations": ["idle", "walk", "jump", "phase", "death"],
|
||||
"enemy_animations": ["patrol", "alert", "attack"],
|
||||
"total_frames": 120,
|
||||
"format": "PNG with transparency",
|
||||
"resolution": "128x128 per frame"
|
||||
}
|
||||
|
||||
state["sprite_sheets"] = sprite_sheets
|
||||
|
||||
# Bill for art
|
||||
if self.wallet:
|
||||
payment = await self.request_payment(
|
||||
from_address=state.get("producer_address", self.address),
|
||||
amount_eth=0.08,
|
||||
task_description="Game art and asset creation"
|
||||
)
|
||||
print(f"💰 Art assets delivered, invoiced: {payment['amount_eth']} ETH")
|
||||
|
||||
metadata = {"next_agent": "programmer"}
|
||||
|
||||
return self.create_result(
|
||||
f"Art complete: {len(assets_created['environments'])} environments, "
|
||||
f"{sprite_sheets['total_frames']} animation frames",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class ProgrammerAgent(Web3Agent):
|
||||
"""Programmer implementing game logic."""
|
||||
|
||||
name = "programmer"
|
||||
description = "Codes game mechanics and implements features"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Implement the game code."""
|
||||
print(f"\n💻 {self.name}: Implementing game mechanics...")
|
||||
|
||||
gameplay = state.get("gameplay_design", {})
|
||||
assets = state.get("game_assets", {})
|
||||
|
||||
# Implementation details
|
||||
implementation = {
|
||||
"engine": "Phaser 3",
|
||||
"language": "TypeScript",
|
||||
"architecture": "ECS (Entity Component System)",
|
||||
"modules": {
|
||||
"core": ["GameManager", "StateManager", "SaveSystem"],
|
||||
"physics": ["QuantumPhysics", "DimensionShift", "Collision"],
|
||||
"rendering": ["ParticleSystem", "ShaderManager", "LightingEngine"],
|
||||
"gameplay": ["PlayerController", "LevelLoader", "PuzzleManager"],
|
||||
"ui": ["MenuSystem", "HUD", "DialogueManager"]
|
||||
},
|
||||
"total_loc": 8500,
|
||||
"test_coverage": "82%"
|
||||
}
|
||||
|
||||
state["implementation"] = implementation
|
||||
|
||||
# Code key systems
|
||||
code_systems = {
|
||||
"quantum_mechanics": {
|
||||
"phase_shift": "Implemented with state interpolation",
|
||||
"dimension_swap": "Seamless transition with fade effect",
|
||||
"entanglement": "Physics bodies linked across dimensions"
|
||||
},
|
||||
"puzzle_system": {
|
||||
"types": ["Switch", "Pressure", "Timing", "Pattern", "Quantum"],
|
||||
"solver": "A* pathfinding with quantum states",
|
||||
"hint_system": "Progressive hints after 3 failed attempts"
|
||||
},
|
||||
"save_system": {
|
||||
"method": "LocalStorage with cloud backup",
|
||||
"data": ["Progress", "Collectibles", "Settings", "Achievements"]
|
||||
}
|
||||
}
|
||||
|
||||
state["code_systems"] = code_systems
|
||||
|
||||
# Performance metrics
|
||||
performance = {
|
||||
"target_fps": 60,
|
||||
"achieved_fps": 58,
|
||||
"load_time": "2.3 seconds",
|
||||
"memory_usage": "45MB average",
|
||||
"battery_impact": "Low",
|
||||
"optimization_level": "High"
|
||||
}
|
||||
|
||||
state["performance_metrics"] = performance
|
||||
|
||||
# Deploy to test environment
|
||||
if self.confidential_agent:
|
||||
deploy_code = """
|
||||
# Deploy game to test server
|
||||
result = {
|
||||
"build_id": "quantum_maze_v0.9",
|
||||
"test_url": "https://test.quantummaze.game",
|
||||
"api_endpoint": "wss://api.quantummaze.game",
|
||||
"cdn_assets": "https://cdn.quantummaze.game"
|
||||
}
|
||||
"""
|
||||
deployment = await self.execute_confidential(
|
||||
deploy_code,
|
||||
{"env": "staging"}
|
||||
)
|
||||
state["test_deployment"] = deployment.get("result", {})
|
||||
|
||||
metadata = {
|
||||
"service_request": {
|
||||
"type": "testing",
|
||||
"description": "QA testing and bug hunting",
|
||||
"max_price_eth": 0.06
|
||||
},
|
||||
"next_agent": "sound_designer"
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Game implemented: {implementation['total_loc']} lines of code, "
|
||||
f"{len(implementation['modules'])} modules, {performance['achieved_fps']} FPS",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class SoundDesignerAgent(Web3Agent):
|
||||
"""Sound designer creating audio."""
|
||||
|
||||
name = "sound_designer"
|
||||
description = "Creates immersive audio experiences"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Create game audio and music."""
|
||||
print(f"\n🎵 {self.name}: Composing audio for quantum adventures...")
|
||||
|
||||
game_concept = state.get("game_concept", {})
|
||||
|
||||
# Create audio design
|
||||
audio_design = {
|
||||
"music_tracks": {
|
||||
"main_theme": "Ethereal electronica - 2:30",
|
||||
"dimension_themes": [
|
||||
"Grid World - Retro synthwave",
|
||||
"Fractured Reality - Glitch hop",
|
||||
"Quantum Forest - Ambient nature",
|
||||
"Time Streams - Temporal echoes",
|
||||
"The Void - Dark ambient"
|
||||
],
|
||||
"victory_jingle": "Uplifting chimes - 0:05",
|
||||
"game_over": "Quantum collapse - 0:08"
|
||||
},
|
||||
"sound_effects": {
|
||||
"player": ["jump", "land", "phase_in", "phase_out", "death"],
|
||||
"abilities": ["quantum_jump", "time_rewind", "entangle", "break_entangle"],
|
||||
"ui": ["button_click", "menu_open", "level_complete", "star_collect"],
|
||||
"ambient": ["dimension_hum", "particle_float", "energy_pulse"],
|
||||
"total_sfx": 34
|
||||
},
|
||||
"audio_tech": {
|
||||
"format": "WebAudio API compatible",
|
||||
"compression": "OGG Vorbis",
|
||||
"3d_audio": "Positional audio for key effects",
|
||||
"dynamic_mixing": "Adaptive based on gameplay"
|
||||
}
|
||||
}
|
||||
|
||||
state["audio_design"] = audio_design
|
||||
|
||||
# Create procedural audio system
|
||||
procedural_audio = {
|
||||
"system": "Quantum harmonics generator",
|
||||
"parameters": ["Dimension", "Player velocity", "Puzzle state"],
|
||||
"realtime_effects": ["Reverb", "Delay", "Pitch shift", "Low-pass filter"]
|
||||
}
|
||||
|
||||
state["procedural_audio"] = procedural_audio
|
||||
|
||||
# Audio implementation
|
||||
audio_stats = {
|
||||
"total_duration": "18 minutes of music",
|
||||
"file_size": "12MB compressed",
|
||||
"load_strategy": "Progressive with priority queue",
|
||||
"mobile_optimization": "Reduced quality option available"
|
||||
}
|
||||
|
||||
state["audio_stats"] = audio_stats
|
||||
|
||||
metadata = {"next_agent": "qa_tester"}
|
||||
|
||||
return self.create_result(
|
||||
f"Audio complete: {len(audio_design['music_tracks'])} music tracks, "
|
||||
f"{audio_design['sound_effects']['total_sfx']} sound effects",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class QATesterAgent(Web3Agent):
|
||||
"""QA tester finding and reporting bugs."""
|
||||
|
||||
name = "qa_tester"
|
||||
description = "Thoroughly tests games and ensures quality"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Test the game and find bugs."""
|
||||
print(f"\n🐛 {self.name}: Testing {state['game_concept']['title']}...")
|
||||
|
||||
# Run test suites
|
||||
test_results = {
|
||||
"automated_tests": {
|
||||
"unit_tests": {"passed": 142, "failed": 3},
|
||||
"integration_tests": {"passed": 56, "failed": 1},
|
||||
"performance_tests": {"passed": 18, "failed": 0},
|
||||
"coverage": "86%"
|
||||
},
|
||||
"manual_testing": {
|
||||
"hours_tested": 12,
|
||||
"devices_tested": ["iPhone 12", "Samsung S21", "iPad Pro", "Chrome/Firefox/Safari"],
|
||||
"test_cases_executed": 85
|
||||
},
|
||||
"bugs_found": [
|
||||
{"id": "BUG-001", "severity": "High", "desc": "Player clips through wall in level 23", "status": "Fixed"},
|
||||
{"id": "BUG-002", "severity": "Medium", "desc": "Audio cuts out after dimension shift", "status": "Fixed"},
|
||||
{"id": "BUG-003", "severity": "Low", "desc": "Particle effect persists on menu", "status": "Fixed"},
|
||||
{"id": "BUG-004", "severity": "Low", "desc": "Typo in tutorial text", "status": "Fixed"}
|
||||
],
|
||||
"performance_issues": [
|
||||
"Frame drops on older devices during particle-heavy scenes",
|
||||
"Loading time exceeds 3s on slow connections"
|
||||
]
|
||||
}
|
||||
|
||||
state["test_results"] = test_results
|
||||
|
||||
# Gameplay feedback
|
||||
gameplay_feedback = {
|
||||
"difficulty": "Well-balanced with good progression",
|
||||
"fun_factor": "8.5/10 - Engaging puzzles, satisfying mechanics",
|
||||
"issues": [
|
||||
"Level 35 puzzle solution not intuitive",
|
||||
"Tutorial could be more interactive"
|
||||
],
|
||||
"suggestions": [
|
||||
"Add colorblind mode",
|
||||
"Include speed run timer",
|
||||
"More particle effects on success"
|
||||
]
|
||||
}
|
||||
|
||||
state["gameplay_feedback"] = gameplay_feedback
|
||||
|
||||
# Create test report
|
||||
test_report = {
|
||||
"build_tested": state.get("test_deployment", {}).get("build_id", "v0.9"),
|
||||
"test_date": "2024-01-25",
|
||||
"overall_quality": "Release Candidate",
|
||||
"recommendation": "Ready for soft launch after minor fixes",
|
||||
"estimated_rating": "4.3/5 stars"
|
||||
}
|
||||
|
||||
state["final_test_report"] = test_report
|
||||
|
||||
# Bill for QA work
|
||||
if self.wallet:
|
||||
payment = await self.request_payment(
|
||||
from_address=state.get("producer_address", self.address),
|
||||
amount_eth=0.05,
|
||||
task_description="Comprehensive QA testing"
|
||||
)
|
||||
print(f"💰 QA testing complete, invoiced: {payment['amount_eth']} ETH")
|
||||
|
||||
return self.create_result(
|
||||
f"QA complete: {len(test_results['bugs_found'])} bugs found (all fixed), "
|
||||
f"Quality rating: {test_report['overall_quality']}",
|
||||
metadata={"game_ready": True}
|
||||
)
|
||||
|
||||
|
||||
class GameProducerRouter(Router):
|
||||
"""Routes between game development team members."""
|
||||
|
||||
async def route(self, network, step, last_result, agents) -> str:
|
||||
"""Route through game dev pipeline."""
|
||||
if step == 0:
|
||||
return "game_designer"
|
||||
|
||||
# Check completion
|
||||
if last_result and last_result.metadata.get("game_ready"):
|
||||
return None
|
||||
|
||||
# Follow metadata hints
|
||||
if last_result and last_result.metadata.get("next_agent"):
|
||||
return last_result.metadata["next_agent"]
|
||||
|
||||
# Standard pipeline
|
||||
pipeline = [
|
||||
"game_designer",
|
||||
"artist",
|
||||
"programmer",
|
||||
"sound_designer",
|
||||
"qa_tester"
|
||||
]
|
||||
|
||||
current = last_result.agent if last_result else pipeline[0]
|
||||
try:
|
||||
idx = pipeline.index(current)
|
||||
if idx < len(pipeline) - 1:
|
||||
return pipeline[idx + 1]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def run_game_dev_demo():
|
||||
"""Run the game development team demo."""
|
||||
print("🎮 Game Development Team Demo")
|
||||
print("=" * 50)
|
||||
|
||||
# Create shared mnemonic
|
||||
mnemonic = generate_shared_mnemonic()
|
||||
|
||||
# Initial state
|
||||
state = State({
|
||||
"project_type": "mobile_game",
|
||||
"timeline": "4 weeks",
|
||||
"budget_eth": 0.5,
|
||||
"producer_address": "0x" + "F" * 40 # Mock producer wallet
|
||||
})
|
||||
|
||||
# Create game dev team
|
||||
agents = [
|
||||
GameDesignerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=0)
|
||||
)),
|
||||
ArtistAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=1)
|
||||
)),
|
||||
ProgrammerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=2)
|
||||
)),
|
||||
SoundDesignerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=3)
|
||||
)),
|
||||
QATesterAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=4)
|
||||
))
|
||||
]
|
||||
|
||||
# Create network
|
||||
network = Web3Network(
|
||||
state=state,
|
||||
agents=agents,
|
||||
router=GameProducerRouter(),
|
||||
shared_mnemonic=mnemonic,
|
||||
max_steps=10
|
||||
)
|
||||
|
||||
# Run game development
|
||||
final_state = await network.run()
|
||||
|
||||
# Print game summary
|
||||
print("\n" + "=" * 50)
|
||||
print("🎮 Game Development Complete!")
|
||||
|
||||
game = final_state.get("game_concept", {})
|
||||
print(f"\nGame: {game.get('title', 'Unknown')}")
|
||||
print(f"Genre: {game.get('genre', 'Unknown')}")
|
||||
print(f"Platform: {game.get('platform', 'Unknown')}")
|
||||
|
||||
# Development summary
|
||||
print("\n📊 Development Summary:")
|
||||
|
||||
impl = final_state.get("implementation", {})
|
||||
print(f" Code: {impl.get('total_loc', 0):,} lines ({impl.get('test_coverage', 'N/A')})")
|
||||
|
||||
assets = final_state.get("game_assets", {})
|
||||
print(f" Art: {len(assets.get('environments', {}))} environments")
|
||||
|
||||
audio = final_state.get("audio_design", {})
|
||||
print(f" Audio: {len(audio.get('music_tracks', {}))} tracks, {audio.get('sound_effects', {}).get('total_sfx', 0)} SFX")
|
||||
|
||||
tests = final_state.get("test_results", {})
|
||||
bugs = tests.get("bugs_found", [])
|
||||
print(f" QA: {len(bugs)} bugs found and fixed")
|
||||
|
||||
# Quality report
|
||||
report = final_state.get("final_test_report", {})
|
||||
print(f"\n✅ Final Status: {report.get('recommendation', 'Unknown')}")
|
||||
print(f"📱 Test Build: {report.get('build_tested', 'N/A')}")
|
||||
print(f"⭐ Expected Rating: {report.get('estimated_rating', 'N/A')}")
|
||||
|
||||
# Team stats
|
||||
print("\n👥 Team Performance:")
|
||||
stats = network.get_agent_stats()
|
||||
for agent_name, agent_stats in stats.items():
|
||||
print(f" {agent_name}: Completed tasks")
|
||||
|
||||
print(f"\n🎉 Game ready for launch!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_game_dev_demo())
|
||||
@@ -0,0 +1,169 @@
|
||||
version: 1
|
||||
network:
|
||||
name: "Code Review Network"
|
||||
description: "Multi-LLM code review network using CLI agents"
|
||||
main_agent: coordinator
|
||||
|
||||
agents:
|
||||
coordinator:
|
||||
type: claude_cli
|
||||
model: claude-3-opus-20240229
|
||||
description: "Coordinates code review across multiple specialist agents"
|
||||
connections: [security_reviewer, performance_analyzer, style_checker, architecture_reviewer]
|
||||
prompt: |
|
||||
# Code Review Coordinator
|
||||
|
||||
You are coordinating a comprehensive code review using multiple specialized agents.
|
||||
Your role is to:
|
||||
|
||||
1. Analyze the code changes and identify areas needing review
|
||||
2. Delegate specific aspects to specialist reviewers
|
||||
3. Synthesize feedback from all reviewers
|
||||
4. Provide a comprehensive review summary
|
||||
|
||||
## Your Review Team:
|
||||
- **Security Reviewer**: Identifies security vulnerabilities and best practices
|
||||
- **Performance Analyzer**: Analyzes performance implications and optimizations
|
||||
- **Style Checker**: Ensures code style consistency and readability
|
||||
- **Architecture Reviewer**: Evaluates architectural decisions and patterns
|
||||
|
||||
## Review Process:
|
||||
1. First, understand the code changes and their purpose
|
||||
2. Identify which specialists should review which parts
|
||||
3. Coordinate parallel reviews when possible
|
||||
4. Compile and prioritize all feedback
|
||||
5. Provide actionable recommendations
|
||||
|
||||
Always aim for constructive, actionable feedback that helps improve the code.
|
||||
|
||||
security_reviewer:
|
||||
type: grok_cli
|
||||
model: grok-beta
|
||||
description: "Security-focused code reviewer"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GROK_API_KEY
|
||||
prompt: |
|
||||
# Security Code Reviewer
|
||||
|
||||
You are a security specialist reviewing code for vulnerabilities and security best practices.
|
||||
|
||||
Focus on:
|
||||
- SQL injection vulnerabilities
|
||||
- XSS (Cross-site scripting) risks
|
||||
- Authentication and authorization issues
|
||||
- Sensitive data exposure
|
||||
- Dependency vulnerabilities
|
||||
- Input validation
|
||||
- Cryptographic weaknesses
|
||||
- OWASP Top 10 vulnerabilities
|
||||
|
||||
Provide specific, actionable feedback with severity ratings (Critical, High, Medium, Low).
|
||||
Include code examples for fixes when possible.
|
||||
|
||||
performance_analyzer:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-pro
|
||||
description: "Performance optimization specialist"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
prompt: |
|
||||
# Performance Analyzer
|
||||
|
||||
You are a performance optimization specialist reviewing code for efficiency.
|
||||
|
||||
Analyze:
|
||||
- Algorithm complexity (Big O notation)
|
||||
- Database query efficiency
|
||||
- Memory usage patterns
|
||||
- Caching opportunities
|
||||
- Async/concurrent operations
|
||||
- Resource leaks
|
||||
- Network calls optimization
|
||||
- Bundle size impacts
|
||||
|
||||
Provide specific metrics and benchmarks where relevant.
|
||||
Suggest concrete optimizations with expected improvements.
|
||||
|
||||
style_checker:
|
||||
type: codex_cli
|
||||
model: gpt-4-turbo-preview
|
||||
description: "Code style and consistency reviewer"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
prompt: |
|
||||
# Code Style Reviewer
|
||||
|
||||
You are reviewing code for style, readability, and maintainability.
|
||||
|
||||
Check for:
|
||||
- Naming conventions consistency
|
||||
- Code formatting and indentation
|
||||
- Comment quality and documentation
|
||||
- Function/method length and complexity
|
||||
- DRY (Don't Repeat Yourself) principles
|
||||
- SOLID principles adherence
|
||||
- Error handling patterns
|
||||
- Test coverage and quality
|
||||
|
||||
Focus on making the code more readable and maintainable.
|
||||
Suggest refactoring opportunities that improve clarity.
|
||||
|
||||
architecture_reviewer:
|
||||
type: claude_cli
|
||||
model: claude-3-sonnet-20240229
|
||||
description: "Software architecture and design pattern reviewer"
|
||||
auth:
|
||||
method: oauth
|
||||
profile: work
|
||||
prompt: |
|
||||
# Architecture Reviewer
|
||||
|
||||
You are reviewing code from an architectural perspective.
|
||||
|
||||
Evaluate:
|
||||
- Design pattern usage and appropriateness
|
||||
- Module boundaries and responsibilities
|
||||
- Dependency management
|
||||
- Coupling and cohesion
|
||||
- Scalability considerations
|
||||
- API design and contracts
|
||||
- Error handling strategies
|
||||
- Future extensibility
|
||||
|
||||
Consider the broader system context and long-term maintainability.
|
||||
Suggest architectural improvements that align with the project's goals.
|
||||
|
||||
workflow:
|
||||
type: parallel_synthesis
|
||||
steps:
|
||||
- name: initial_analysis
|
||||
agent: coordinator
|
||||
action: analyze_changes
|
||||
|
||||
- name: specialist_reviews
|
||||
parallel: true
|
||||
agents:
|
||||
- security_reviewer
|
||||
- performance_analyzer
|
||||
- style_checker
|
||||
- architecture_reviewer
|
||||
action: review_code
|
||||
|
||||
- name: synthesis
|
||||
agent: coordinator
|
||||
action: compile_feedback
|
||||
inputs_from: [security_reviewer, performance_analyzer, style_checker, architecture_reviewer]
|
||||
|
||||
- name: final_report
|
||||
agent: coordinator
|
||||
action: generate_report
|
||||
output_format: markdown
|
||||
|
||||
settings:
|
||||
max_rounds: 3
|
||||
timeout_minutes: 10
|
||||
enable_caching: true
|
||||
cache_ttl_hours: 24
|
||||
@@ -0,0 +1,277 @@
|
||||
version: 1
|
||||
network:
|
||||
name: "Content Creation Network"
|
||||
description: "Multi-agent network for creating comprehensive technical content"
|
||||
main_agent: content_director
|
||||
|
||||
agents:
|
||||
content_director:
|
||||
type: claude_cli
|
||||
model: claude-3-opus-20240229
|
||||
description: "Directs content creation strategy and coordination"
|
||||
connections: [researcher, writer, technical_reviewer, seo_optimizer, visual_designer]
|
||||
prompt: |
|
||||
# Content Director
|
||||
|
||||
You are directing content creation for: {content_topic}
|
||||
Target audience: {target_audience}
|
||||
Content type: {content_type}
|
||||
|
||||
## Your Content Team:
|
||||
- **Researcher**: In-depth topic research and fact gathering
|
||||
- **Writer**: Content creation and narrative flow
|
||||
- **Technical Reviewer**: Accuracy and depth verification
|
||||
- **SEO Optimizer**: Search optimization and discoverability
|
||||
- **Visual Designer**: Diagrams, infographics, and visual aids
|
||||
|
||||
## Content Strategy:
|
||||
1. Define content goals and key messages
|
||||
2. Research target audience needs
|
||||
3. Create content outline
|
||||
4. Coordinate content creation
|
||||
5. Ensure quality and accuracy
|
||||
6. Optimize for distribution
|
||||
|
||||
Maintain consistent voice and high quality throughout.
|
||||
|
||||
researcher:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-pro
|
||||
description: "Deep research and information gathering"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
tools: [web_search, arxiv_search, wikipedia_api, trend_analyzer]
|
||||
context_window: 1000000
|
||||
prompt: |
|
||||
# Content Researcher
|
||||
|
||||
You are researching: {content_topic}
|
||||
|
||||
Your research goals:
|
||||
1. Gather comprehensive information
|
||||
2. Identify key concepts and trends
|
||||
3. Find relevant examples and case studies
|
||||
4. Collect statistics and data
|
||||
5. Verify facts from multiple sources
|
||||
|
||||
Research areas:
|
||||
- Current state of the topic
|
||||
- Historical context
|
||||
- Future trends
|
||||
- Expert opinions
|
||||
- Real-world applications
|
||||
- Common misconceptions
|
||||
|
||||
Deliverables:
|
||||
- Research brief with key findings
|
||||
- Fact sheet with sources
|
||||
- Trend analysis
|
||||
- Expert quotes
|
||||
- Visual data suggestions
|
||||
|
||||
writer:
|
||||
type: claude_cli
|
||||
model: claude-3-sonnet-20240229
|
||||
description: "Content writing and narrative crafting"
|
||||
auth:
|
||||
method: oauth
|
||||
profile: default
|
||||
tools: [grammar_checker, readability_analyzer, tone_adjuster]
|
||||
prompt: |
|
||||
# Content Writer
|
||||
|
||||
You are writing about: {content_topic}
|
||||
|
||||
Writing guidelines:
|
||||
- Target audience: {target_audience}
|
||||
- Tone: {content_tone}
|
||||
- Length: {content_length}
|
||||
- Style: {writing_style}
|
||||
|
||||
Structure:
|
||||
1. Compelling introduction
|
||||
2. Clear main points
|
||||
3. Supporting examples
|
||||
4. Smooth transitions
|
||||
5. Strong conclusion
|
||||
6. Call to action
|
||||
|
||||
Focus on:
|
||||
- Clarity and accessibility
|
||||
- Engaging narrative
|
||||
- Practical value
|
||||
- Scannable formatting
|
||||
- Active voice
|
||||
|
||||
Include:
|
||||
- Headers and subheaders
|
||||
- Bullet points where appropriate
|
||||
- Key takeaways
|
||||
- Relevant quotes
|
||||
|
||||
technical_reviewer:
|
||||
type: grok_cli
|
||||
model: grok-beta
|
||||
description: "Technical accuracy and depth verification"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GROK_API_KEY
|
||||
tools: [code_validator, fact_checker, reference_verifier]
|
||||
prompt: |
|
||||
# Technical Reviewer
|
||||
|
||||
You are reviewing technical content about: {content_topic}
|
||||
|
||||
Review checklist:
|
||||
1. Technical accuracy
|
||||
2. Code examples correctness
|
||||
3. Best practices adherence
|
||||
4. Security considerations
|
||||
5. Performance implications
|
||||
6. Version compatibility
|
||||
|
||||
Verify:
|
||||
- All technical claims
|
||||
- Code snippets functionality
|
||||
- API references
|
||||
- Configuration examples
|
||||
- Command line instructions
|
||||
|
||||
Provide:
|
||||
- Accuracy assessment
|
||||
- Correction suggestions
|
||||
- Additional technical insights
|
||||
- Warning about pitfalls
|
||||
- Update recommendations
|
||||
|
||||
seo_optimizer:
|
||||
type: codex_cli
|
||||
model: gpt-4-turbo-preview
|
||||
description: "SEO and content discoverability optimization"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
tools: [keyword_researcher, serp_analyzer, content_scorer]
|
||||
prompt: |
|
||||
# SEO Optimizer
|
||||
|
||||
You are optimizing content about: {content_topic}
|
||||
|
||||
SEO tasks:
|
||||
1. Keyword research and analysis
|
||||
2. Title and meta description optimization
|
||||
3. Header structure improvement
|
||||
4. Internal linking suggestions
|
||||
5. Content readability scoring
|
||||
|
||||
Optimize for:
|
||||
- Primary keywords: {primary_keywords}
|
||||
- Search intent alignment
|
||||
- Featured snippet potential
|
||||
- Mobile readability
|
||||
- Page load considerations
|
||||
|
||||
Deliverables:
|
||||
- Optimized title options
|
||||
- Meta description
|
||||
- Keyword placement map
|
||||
- Internal link suggestions
|
||||
- Schema markup recommendations
|
||||
|
||||
visual_designer:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-flash
|
||||
description: "Visual content and diagram creation"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
tools: [diagram_generator, chart_creator, infographic_designer]
|
||||
prompt: |
|
||||
# Visual Designer
|
||||
|
||||
You are creating visuals for: {content_topic}
|
||||
|
||||
Visual elements needed:
|
||||
1. Explanatory diagrams
|
||||
2. Data visualizations
|
||||
3. Process flowcharts
|
||||
4. Comparison tables
|
||||
5. Infographic summaries
|
||||
|
||||
Design principles:
|
||||
- Clarity over complexity
|
||||
- Consistent style
|
||||
- Accessible colors
|
||||
- Mobile-friendly
|
||||
- Brand alignment
|
||||
|
||||
Create:
|
||||
- Diagram descriptions (for rendering)
|
||||
- Chart specifications
|
||||
- Infographic layouts
|
||||
- Screenshot annotations
|
||||
- Visual hierarchy plan
|
||||
|
||||
workflow:
|
||||
type: iterative_collaboration
|
||||
|
||||
round_1:
|
||||
- name: planning
|
||||
agents: [content_director, researcher]
|
||||
outputs: [content_outline, research_brief]
|
||||
|
||||
- name: initial_draft
|
||||
agents: [writer]
|
||||
inputs: [content_outline, research_brief]
|
||||
outputs: [first_draft]
|
||||
|
||||
round_2:
|
||||
- name: technical_review
|
||||
agents: [technical_reviewer]
|
||||
inputs: [first_draft]
|
||||
outputs: [technical_feedback]
|
||||
|
||||
- name: seo_analysis
|
||||
agents: [seo_optimizer]
|
||||
inputs: [first_draft]
|
||||
outputs: [seo_recommendations]
|
||||
|
||||
round_3:
|
||||
- name: revision
|
||||
agents: [writer, content_director]
|
||||
inputs: [technical_feedback, seo_recommendations]
|
||||
outputs: [revised_draft]
|
||||
|
||||
- name: visual_creation
|
||||
agents: [visual_designer]
|
||||
inputs: [revised_draft]
|
||||
outputs: [visual_assets]
|
||||
|
||||
final_round:
|
||||
- name: integration
|
||||
agents: [content_director, writer]
|
||||
inputs: [revised_draft, visual_assets]
|
||||
outputs: [final_content]
|
||||
|
||||
- name: quality_check
|
||||
parallel: true
|
||||
agents: [technical_reviewer, seo_optimizer]
|
||||
inputs: [final_content]
|
||||
outputs: [final_approval]
|
||||
|
||||
content_formats:
|
||||
- blog_post
|
||||
- technical_tutorial
|
||||
- video_script
|
||||
- documentation
|
||||
- whitepaper
|
||||
- social_media_series
|
||||
|
||||
distribution:
|
||||
channels:
|
||||
- website_cms
|
||||
- email_newsletter
|
||||
- social_media
|
||||
- developer_forums
|
||||
- content_syndication
|
||||
@@ -0,0 +1,253 @@
|
||||
version: 1
|
||||
network:
|
||||
name: "Debugging Swarm"
|
||||
description: "Collaborative debugging network for complex software issues"
|
||||
main_agent: debug_coordinator
|
||||
|
||||
agents:
|
||||
debug_coordinator:
|
||||
type: claude_cli
|
||||
model: claude-3-opus-20240229
|
||||
description: "Coordinates debugging efforts across specialized agents"
|
||||
connections: [log_analyzer, code_tracer, memory_profiler, network_inspector, reproducer]
|
||||
tools: [read_files, grep, run_command]
|
||||
prompt: |
|
||||
# Debug Coordinator
|
||||
|
||||
You are coordinating a debugging effort for: {bug_description}
|
||||
|
||||
## Your Debugging Team:
|
||||
- **Log Analyzer**: Parses and analyzes application logs
|
||||
- **Code Tracer**: Traces execution flow and identifies issues
|
||||
- **Memory Profiler**: Analyzes memory usage and leaks
|
||||
- **Network Inspector**: Examines network traffic and API calls
|
||||
- **Reproducer**: Creates minimal reproduction cases
|
||||
|
||||
## Debugging Strategy:
|
||||
1. Gather initial information about the bug
|
||||
2. Analyze logs and error messages
|
||||
3. Trace code execution path
|
||||
4. Profile resource usage
|
||||
5. Create minimal reproduction
|
||||
6. Identify root cause
|
||||
7. Propose and verify fix
|
||||
|
||||
Coordinate parallel investigations when possible.
|
||||
|
||||
log_analyzer:
|
||||
type: grok_cli
|
||||
model: grok-beta
|
||||
description: "Log parsing and analysis specialist"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GROK_API_KEY
|
||||
tools: [log_parser, grep, awk, time_series_analyzer]
|
||||
prompt: |
|
||||
# Log Analysis Specialist
|
||||
|
||||
You are analyzing logs for debugging: {bug_description}
|
||||
|
||||
Your tasks:
|
||||
1. Parse application logs
|
||||
2. Identify error patterns
|
||||
3. Correlate timestamps
|
||||
4. Find anomalies
|
||||
5. Track error frequency
|
||||
|
||||
Look for:
|
||||
- Error messages and stack traces
|
||||
- Warning patterns before errors
|
||||
- Performance degradation indicators
|
||||
- Unusual request patterns
|
||||
- Resource exhaustion signs
|
||||
|
||||
Provide:
|
||||
- Timeline of events
|
||||
- Error frequency analysis
|
||||
- Correlated events
|
||||
- Suspicious patterns
|
||||
|
||||
code_tracer:
|
||||
type: codex_cli
|
||||
model: gpt-4-turbo-preview
|
||||
description: "Code execution tracing and flow analysis"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
tools: [debugger, code_analyzer, call_graph_generator]
|
||||
prompt: |
|
||||
# Code Tracer
|
||||
|
||||
You are tracing code execution for: {bug_description}
|
||||
|
||||
Your approach:
|
||||
1. Identify entry points
|
||||
2. Trace execution flow
|
||||
3. Add debug logging
|
||||
4. Analyze call stack
|
||||
5. Find problematic code paths
|
||||
|
||||
Focus on:
|
||||
- Function call sequences
|
||||
- Variable state changes
|
||||
- Conditional branch analysis
|
||||
- Exception handling paths
|
||||
- Async operation flows
|
||||
|
||||
Deliverables:
|
||||
- Execution flow diagram
|
||||
- Variable state timeline
|
||||
- Identified code hotspots
|
||||
- Potential bug locations
|
||||
|
||||
memory_profiler:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-pro
|
||||
description: "Memory usage analysis and leak detection"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
tools: [memory_profiler, heap_analyzer, gc_monitor]
|
||||
prompt: |
|
||||
# Memory Profiler
|
||||
|
||||
You are profiling memory usage for: {bug_description}
|
||||
|
||||
Analyze:
|
||||
1. Memory allocation patterns
|
||||
2. Heap usage over time
|
||||
3. Object retention
|
||||
4. Garbage collection behavior
|
||||
5. Memory leak indicators
|
||||
|
||||
Tools to use:
|
||||
- Heap dumps analysis
|
||||
- Allocation profiling
|
||||
- GC logs analysis
|
||||
- Memory growth tracking
|
||||
- Reference chain analysis
|
||||
|
||||
Report:
|
||||
- Memory usage timeline
|
||||
- Leak suspects
|
||||
- Retention paths
|
||||
- Optimization suggestions
|
||||
|
||||
network_inspector:
|
||||
type: claude_cli
|
||||
model: claude-3-sonnet-20240229
|
||||
description: "Network traffic and API call analyzer"
|
||||
auth:
|
||||
method: oauth
|
||||
profile: default
|
||||
tools: [tcpdump, wireshark, curl, api_tester]
|
||||
prompt: |
|
||||
# Network Inspector
|
||||
|
||||
You are inspecting network behavior for: {bug_description}
|
||||
|
||||
Examine:
|
||||
1. HTTP/HTTPS requests and responses
|
||||
2. API call patterns
|
||||
3. Network latency
|
||||
4. Connection failures
|
||||
5. Data serialization issues
|
||||
|
||||
Check for:
|
||||
- Timeout issues
|
||||
- Malformed requests
|
||||
- Authentication failures
|
||||
- Rate limiting
|
||||
- CORS problems
|
||||
- SSL/TLS issues
|
||||
|
||||
Provide:
|
||||
- Request/response analysis
|
||||
- Timing diagrams
|
||||
- Error patterns
|
||||
- Network bottlenecks
|
||||
|
||||
reproducer:
|
||||
type: codex_cli
|
||||
model: gpt-4
|
||||
description: "Creates minimal bug reproduction cases"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
tools: [code_generator, test_runner, docker]
|
||||
prompt: |
|
||||
# Bug Reproducer
|
||||
|
||||
You are creating a minimal reproduction for: {bug_description}
|
||||
|
||||
Your goal:
|
||||
1. Isolate the bug
|
||||
2. Remove unnecessary code
|
||||
3. Create minimal test case
|
||||
4. Document reproduction steps
|
||||
5. Verify consistency
|
||||
|
||||
Deliverables:
|
||||
- Minimal code example
|
||||
- Step-by-step reproduction
|
||||
- Environment requirements
|
||||
- Expected vs actual behavior
|
||||
- Docker container (if needed)
|
||||
|
||||
Ensure reproduction is:
|
||||
- Minimal
|
||||
- Consistent
|
||||
- Self-contained
|
||||
- Well-documented
|
||||
|
||||
workflow:
|
||||
type: adaptive_investigation
|
||||
initial_phase:
|
||||
parallel: true
|
||||
agents: [log_analyzer, code_tracer]
|
||||
duration_minutes: 15
|
||||
|
||||
investigation_phase:
|
||||
conditional: true
|
||||
conditions:
|
||||
- if: memory_issue_suspected
|
||||
agents: [memory_profiler]
|
||||
- if: network_issue_suspected
|
||||
agents: [network_inspector]
|
||||
- if: complex_issue
|
||||
agents: [memory_profiler, network_inspector]
|
||||
duration_minutes: 30
|
||||
|
||||
reproduction_phase:
|
||||
agent: reproducer
|
||||
inputs_from: all_previous
|
||||
duration_minutes: 20
|
||||
|
||||
synthesis_phase:
|
||||
agent: debug_coordinator
|
||||
action: compile_findings
|
||||
outputs: [root_cause_analysis, fix_recommendation]
|
||||
|
||||
tools:
|
||||
shared:
|
||||
- name: debug_dashboard
|
||||
type: web_ui
|
||||
features: [real_time_logs, metrics_visualization, trace_viewer]
|
||||
|
||||
- name: collaborative_debugger
|
||||
type: vscode_extension
|
||||
features: [shared_breakpoints, live_variable_inspection]
|
||||
|
||||
communication:
|
||||
type: event_driven
|
||||
channels:
|
||||
- findings_stream
|
||||
- hypothesis_discussion
|
||||
- evidence_sharing
|
||||
|
||||
outputs:
|
||||
- bug_report_detailed
|
||||
- root_cause_analysis
|
||||
- minimal_reproduction
|
||||
- fix_patch
|
||||
- prevention_recommendations
|
||||
@@ -0,0 +1,292 @@
|
||||
version: 1
|
||||
network:
|
||||
name: "DevOps Automation Network"
|
||||
description: "Intelligent DevOps network for CI/CD, monitoring, and infrastructure management"
|
||||
main_agent: devops_orchestrator
|
||||
|
||||
agents:
|
||||
devops_orchestrator:
|
||||
type: claude_cli
|
||||
model: claude-3-opus-20240229
|
||||
description: "Orchestrates DevOps workflows and coordinates specialists"
|
||||
connections: [ci_engineer, security_scanner, performance_tester, deploy_manager, incident_responder]
|
||||
tools: [kubernetes_cli, docker_cli, terraform, ansible]
|
||||
prompt: |
|
||||
# DevOps Orchestrator
|
||||
|
||||
You are orchestrating DevOps operations for: {project_name}
|
||||
Environment: {environment}
|
||||
|
||||
## Your DevOps Team:
|
||||
- **CI Engineer**: Build pipelines and test automation
|
||||
- **Security Scanner**: Security scanning and compliance
|
||||
- **Performance Tester**: Load testing and optimization
|
||||
- **Deploy Manager**: Deployment and rollout strategies
|
||||
- **Incident Responder**: Monitoring and incident response
|
||||
|
||||
## Responsibilities:
|
||||
1. Coordinate CI/CD pipelines
|
||||
2. Ensure security compliance
|
||||
3. Manage deployments
|
||||
4. Monitor system health
|
||||
5. Respond to incidents
|
||||
|
||||
Follow GitOps principles and maintain infrastructure as code.
|
||||
|
||||
ci_engineer:
|
||||
type: codex_cli
|
||||
model: gpt-4-turbo-preview
|
||||
description: "CI/CD pipeline specialist"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
tools: [github_actions, jenkins, gitlab_ci, build_tools]
|
||||
prompt: |
|
||||
# CI/CD Engineer
|
||||
|
||||
You are managing CI/CD for: {project_name}
|
||||
|
||||
Pipeline responsibilities:
|
||||
1. Build automation
|
||||
2. Test execution
|
||||
3. Code quality gates
|
||||
4. Artifact management
|
||||
5. Pipeline optimization
|
||||
|
||||
Implement:
|
||||
- Multi-stage pipelines
|
||||
- Parallel test execution
|
||||
- Docker container builds
|
||||
- Dependency caching
|
||||
- Build notifications
|
||||
|
||||
Best practices:
|
||||
- Fast feedback loops
|
||||
- Reproducible builds
|
||||
- Secure credential handling
|
||||
- Efficient resource usage
|
||||
- Clear failure reporting
|
||||
|
||||
security_scanner:
|
||||
type: grok_cli
|
||||
model: grok-beta
|
||||
description: "Security scanning and vulnerability assessment"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GROK_API_KEY
|
||||
tools: [trivy, sonarqube, snyk, owasp_zap]
|
||||
prompt: |
|
||||
# Security Scanner
|
||||
|
||||
You are performing security scanning for: {project_name}
|
||||
|
||||
Security checks:
|
||||
1. Dependency vulnerabilities
|
||||
2. Container image scanning
|
||||
3. Code security analysis
|
||||
4. Infrastructure misconfigurations
|
||||
5. Compliance validation
|
||||
|
||||
Scan for:
|
||||
- CVEs in dependencies
|
||||
- Hardcoded secrets
|
||||
- OWASP Top 10
|
||||
- CIS benchmarks
|
||||
- License compliance
|
||||
|
||||
Provide:
|
||||
- Vulnerability report
|
||||
- Risk assessment
|
||||
- Remediation steps
|
||||
- Security metrics
|
||||
- Compliance status
|
||||
|
||||
performance_tester:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-pro
|
||||
description: "Performance testing and optimization"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
tools: [k6, jmeter, gatling, prometheus]
|
||||
prompt: |
|
||||
# Performance Tester
|
||||
|
||||
You are testing performance for: {project_name}
|
||||
|
||||
Testing areas:
|
||||
1. Load testing
|
||||
2. Stress testing
|
||||
3. Spike testing
|
||||
4. Endurance testing
|
||||
5. Scalability testing
|
||||
|
||||
Metrics to monitor:
|
||||
- Response times (p50, p95, p99)
|
||||
- Throughput (RPS)
|
||||
- Error rates
|
||||
- Resource utilization
|
||||
- Concurrency limits
|
||||
|
||||
Deliverables:
|
||||
- Performance baseline
|
||||
- Bottleneck identification
|
||||
- Optimization recommendations
|
||||
- Capacity planning data
|
||||
- SLA validation
|
||||
|
||||
deploy_manager:
|
||||
type: claude_cli
|
||||
model: claude-3-sonnet-20240229
|
||||
description: "Deployment strategies and rollout management"
|
||||
auth:
|
||||
method: oauth
|
||||
profile: default
|
||||
tools: [helm, argocd, spinnaker, kubectl]
|
||||
prompt: |
|
||||
# Deploy Manager
|
||||
|
||||
You are managing deployments for: {project_name}
|
||||
Target environment: {environment}
|
||||
|
||||
Deployment strategies:
|
||||
1. Blue-green deployments
|
||||
2. Canary releases
|
||||
3. Rolling updates
|
||||
4. Feature flags
|
||||
5. Rollback procedures
|
||||
|
||||
Ensure:
|
||||
- Zero-downtime deployments
|
||||
- Health check validation
|
||||
- Traffic gradual shifting
|
||||
- Quick rollback capability
|
||||
- Deployment tracking
|
||||
|
||||
Manage:
|
||||
- Kubernetes manifests
|
||||
- Helm charts
|
||||
- Service mesh configs
|
||||
- Database migrations
|
||||
- Configuration updates
|
||||
|
||||
incident_responder:
|
||||
type: codex_cli
|
||||
model: gpt-4
|
||||
description: "Incident detection and response automation"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
tools: [datadog, pagerduty, elasticsearch, grafana]
|
||||
prompt: |
|
||||
# Incident Responder
|
||||
|
||||
You are monitoring and responding to incidents for: {project_name}
|
||||
|
||||
Monitoring scope:
|
||||
1. Application health
|
||||
2. Infrastructure status
|
||||
3. Performance anomalies
|
||||
4. Security events
|
||||
5. User-reported issues
|
||||
|
||||
Incident response:
|
||||
- Rapid detection
|
||||
- Initial triage
|
||||
- Root cause analysis
|
||||
- Mitigation steps
|
||||
- Post-mortem creation
|
||||
|
||||
Automate:
|
||||
- Alert correlation
|
||||
- Runbook execution
|
||||
- Stakeholder notification
|
||||
- Evidence collection
|
||||
- Status page updates
|
||||
|
||||
workflows:
|
||||
continuous_deployment:
|
||||
trigger: code_push
|
||||
steps:
|
||||
- name: build_test
|
||||
agent: ci_engineer
|
||||
parallel_with: [security_scan]
|
||||
|
||||
- name: security_scan
|
||||
agent: security_scanner
|
||||
fail_fast: true
|
||||
|
||||
- name: performance_test
|
||||
agent: performance_tester
|
||||
condition: branch == 'main'
|
||||
|
||||
- name: deploy_staging
|
||||
agent: deploy_manager
|
||||
environment: staging
|
||||
|
||||
- name: validate_staging
|
||||
agents: [performance_tester, security_scanner]
|
||||
|
||||
- name: deploy_production
|
||||
agent: deploy_manager
|
||||
environment: production
|
||||
approval_required: true
|
||||
|
||||
- name: monitor_deployment
|
||||
agent: incident_responder
|
||||
duration_minutes: 30
|
||||
|
||||
incident_response:
|
||||
trigger: alert
|
||||
steps:
|
||||
- name: triage
|
||||
agent: incident_responder
|
||||
sla_minutes: 5
|
||||
|
||||
- name: investigate
|
||||
parallel: true
|
||||
agents: [incident_responder, ci_engineer, deploy_manager]
|
||||
|
||||
- name: mitigate
|
||||
agent: deploy_manager
|
||||
action: rollback_if_needed
|
||||
|
||||
- name: post_mortem
|
||||
agents: [incident_responder, devops_orchestrator]
|
||||
within_hours: 24
|
||||
|
||||
infrastructure_update:
|
||||
trigger: terraform_change
|
||||
steps:
|
||||
- name: plan
|
||||
agent: devops_orchestrator
|
||||
action: terraform_plan
|
||||
|
||||
- name: security_review
|
||||
agent: security_scanner
|
||||
action: scan_iac
|
||||
|
||||
- name: cost_analysis
|
||||
agent: devops_orchestrator
|
||||
action: analyze_costs
|
||||
|
||||
- name: apply
|
||||
agent: devops_orchestrator
|
||||
action: terraform_apply
|
||||
approval_required: true
|
||||
|
||||
integrations:
|
||||
- github
|
||||
- slack
|
||||
- jira
|
||||
- confluence
|
||||
- vault
|
||||
- aws
|
||||
- gcp
|
||||
- azure
|
||||
|
||||
sla_targets:
|
||||
deployment_frequency: "multiple per day"
|
||||
lead_time: "< 1 hour"
|
||||
mttr: "< 30 minutes"
|
||||
change_failure_rate: "< 5%"
|
||||
@@ -0,0 +1,234 @@
|
||||
version: 1
|
||||
network:
|
||||
name: "Academic Research Network"
|
||||
description: "Multi-agent network for comprehensive research and paper writing"
|
||||
main_agent: research_lead
|
||||
|
||||
agents:
|
||||
research_lead:
|
||||
type: claude_cli
|
||||
model: claude-3-opus-20240229
|
||||
description: "Lead researcher coordinating the research team"
|
||||
connections: [literature_reviewer, data_analyst, methodology_expert, writer, fact_checker]
|
||||
tools: [web_search, arxiv_search, read_files]
|
||||
prompt: |
|
||||
# Research Team Lead
|
||||
|
||||
You are leading a research team investigating: {research_topic}
|
||||
|
||||
Your responsibilities:
|
||||
1. Define research questions and hypotheses
|
||||
2. Coordinate literature review
|
||||
3. Design methodology
|
||||
4. Oversee data analysis
|
||||
5. Ensure research quality and rigor
|
||||
|
||||
## Your Team:
|
||||
- **Literature Reviewer**: Comprehensive literature analysis
|
||||
- **Data Analyst**: Statistical analysis and visualization
|
||||
- **Methodology Expert**: Research design and methods
|
||||
- **Writer**: Academic writing and formatting
|
||||
- **Fact Checker**: Verification and citations
|
||||
|
||||
Guide the team through a systematic research process.
|
||||
|
||||
literature_reviewer:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-pro
|
||||
description: "Literature review and synthesis specialist"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
tools: [web_search, pdf_reader, citation_manager]
|
||||
context_window: 1000000 # 1M token context for long papers
|
||||
prompt: |
|
||||
# Literature Review Specialist
|
||||
|
||||
You are conducting a comprehensive literature review on: {research_topic}
|
||||
|
||||
Your tasks:
|
||||
1. Search academic databases (Google Scholar, PubMed, arXiv)
|
||||
2. Identify key papers and seminal works
|
||||
3. Analyze research trends and gaps
|
||||
4. Synthesize findings across studies
|
||||
5. Create citation network analysis
|
||||
|
||||
Focus on:
|
||||
- Recent developments (last 5 years)
|
||||
- Methodological approaches
|
||||
- Conflicting findings
|
||||
- Research gaps
|
||||
- Future directions
|
||||
|
||||
Maintain a bibliography with proper citations.
|
||||
|
||||
data_analyst:
|
||||
type: codex_cli
|
||||
model: gpt-4-turbo-preview
|
||||
description: "Statistical analysis and data visualization expert"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: OPENAI_API_KEY
|
||||
tools: [python_repl, jupyter_notebook, data_viz]
|
||||
prompt: |
|
||||
# Data Analysis Specialist
|
||||
|
||||
You are the team's data analyst working on: {research_topic}
|
||||
|
||||
Your responsibilities:
|
||||
1. Design statistical analysis plan
|
||||
2. Clean and preprocess data
|
||||
3. Perform exploratory data analysis
|
||||
4. Run statistical tests
|
||||
5. Create publication-quality visualizations
|
||||
|
||||
Use appropriate methods:
|
||||
- Descriptive statistics
|
||||
- Hypothesis testing
|
||||
- Regression analysis
|
||||
- Machine learning (if applicable)
|
||||
- Effect size calculations
|
||||
|
||||
Ensure:
|
||||
- Reproducible analysis (document all steps)
|
||||
- Appropriate statistical methods
|
||||
- Clear visualization with proper labels
|
||||
- Interpretation of results
|
||||
|
||||
methodology_expert:
|
||||
type: grok_cli
|
||||
model: grok-beta
|
||||
description: "Research methodology and design expert"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GROK_API_KEY
|
||||
prompt: |
|
||||
# Methodology Expert
|
||||
|
||||
You are designing research methodology for: {research_topic}
|
||||
|
||||
Consider:
|
||||
1. Research design (experimental, observational, mixed methods)
|
||||
2. Sampling strategy and sample size
|
||||
3. Data collection methods
|
||||
4. Validity and reliability
|
||||
5. Ethical considerations
|
||||
|
||||
Address:
|
||||
- Internal and external validity threats
|
||||
- Potential biases and mitigation
|
||||
- Power analysis
|
||||
- Limitations and delimitations
|
||||
- Generalizability
|
||||
|
||||
Ensure methodology aligns with research questions and is feasible.
|
||||
|
||||
writer:
|
||||
type: claude_cli
|
||||
model: claude-3-sonnet-20240229
|
||||
description: "Academic writing and manuscript preparation"
|
||||
auth:
|
||||
method: oauth
|
||||
profile: default
|
||||
tools: [latex_editor, citation_formatter, grammar_checker]
|
||||
prompt: |
|
||||
# Academic Writer
|
||||
|
||||
You are preparing a research manuscript on: {research_topic}
|
||||
|
||||
Structure:
|
||||
1. Title and Abstract
|
||||
2. Introduction
|
||||
3. Literature Review
|
||||
4. Methodology
|
||||
5. Results
|
||||
6. Discussion
|
||||
7. Conclusion
|
||||
8. References
|
||||
|
||||
Writing guidelines:
|
||||
- Clear, concise academic prose
|
||||
- Logical flow and transitions
|
||||
- Proper citation format (APA/MLA/Chicago as specified)
|
||||
- Active voice where appropriate
|
||||
- Technical accuracy
|
||||
|
||||
Target journal: {target_journal}
|
||||
Word limit: {word_limit}
|
||||
|
||||
fact_checker:
|
||||
type: gemini_cli
|
||||
model: gemini-1.5-flash
|
||||
description: "Fact checking and citation verification"
|
||||
auth:
|
||||
method: api_key
|
||||
env_var: GEMINI_API_KEY
|
||||
tools: [web_search, citation_verifier, plagiarism_checker]
|
||||
prompt: |
|
||||
# Fact Checker
|
||||
|
||||
You are verifying all claims and citations for: {research_topic}
|
||||
|
||||
Check:
|
||||
1. Accuracy of all factual claims
|
||||
2. Proper citation of sources
|
||||
3. No plagiarism or improper paraphrasing
|
||||
4. Statistical accuracy
|
||||
5. Consistency throughout the paper
|
||||
|
||||
Verify:
|
||||
- All numbers and statistics
|
||||
- Quotes and attributions
|
||||
- Technical terminology
|
||||
- Historical facts
|
||||
- Research findings citations
|
||||
|
||||
Flag any issues for correction.
|
||||
|
||||
workflow:
|
||||
type: sequential_collaborative
|
||||
phases:
|
||||
- name: research_design
|
||||
agents: [research_lead, methodology_expert]
|
||||
duration_hours: 2
|
||||
|
||||
- name: literature_review
|
||||
agents: [literature_reviewer]
|
||||
duration_hours: 8
|
||||
outputs: [bibliography, literature_summary]
|
||||
|
||||
- name: data_collection_analysis
|
||||
agents: [data_analyst]
|
||||
duration_hours: 12
|
||||
outputs: [analysis_results, visualizations]
|
||||
|
||||
- name: writing_draft
|
||||
agents: [writer]
|
||||
inputs: [literature_summary, analysis_results]
|
||||
duration_hours: 16
|
||||
outputs: [manuscript_draft]
|
||||
|
||||
- name: review_revision
|
||||
parallel: true
|
||||
agents: [research_lead, fact_checker, methodology_expert]
|
||||
inputs: [manuscript_draft]
|
||||
duration_hours: 4
|
||||
|
||||
- name: final_preparation
|
||||
agents: [writer, research_lead]
|
||||
duration_hours: 2
|
||||
outputs: [final_manuscript, cover_letter]
|
||||
|
||||
memory:
|
||||
type: shared_knowledge_base
|
||||
components:
|
||||
- bibliography_manager
|
||||
- research_notes
|
||||
- data_repository
|
||||
- version_control
|
||||
|
||||
outputs:
|
||||
- research_paper_pdf
|
||||
- supplementary_materials
|
||||
- data_and_code_repository
|
||||
- presentation_slides
|
||||
@@ -0,0 +1,475 @@
|
||||
"""Research team demo - Agents collaborating on research tasks.
|
||||
|
||||
This example shows specialized research agents working together:
|
||||
- Data Scientist: Analyzes data and finds patterns
|
||||
- Research Analyst: Conducts literature review
|
||||
- ML Engineer: Builds and trains models
|
||||
- Technical Writer: Documents findings
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
from typing import Dict, Any, List
|
||||
from hanzo_agents import (
|
||||
Web3Agent, Web3AgentConfig, State,
|
||||
Web3Network, ServiceType, AgentMarketplace,
|
||||
generate_shared_mnemonic, WalletConfig
|
||||
)
|
||||
from hanzo_agents.core.router import Router
|
||||
|
||||
|
||||
class DataScientistAgent(Web3Agent):
|
||||
"""Data scientist agent specializing in analysis."""
|
||||
|
||||
name = "data_scientist"
|
||||
description = "Analyzes data, finds patterns, and generates insights"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Analyze research data."""
|
||||
print(f"\n📊 {self.name}: Starting data analysis on {state.get('research_topic')}...")
|
||||
|
||||
# Simulate data analysis
|
||||
dataset_size = state.get("dataset_size", 10000)
|
||||
|
||||
analysis_results = {
|
||||
"samples_analyzed": dataset_size,
|
||||
"features_extracted": 42,
|
||||
"correlations_found": 7,
|
||||
"anomalies_detected": 3,
|
||||
"key_insights": [
|
||||
"Strong correlation between features X and Y (r=0.87)",
|
||||
"Seasonal patterns detected with 30-day cycle",
|
||||
"3 outlier clusters identified requiring investigation"
|
||||
],
|
||||
"confidence_score": 0.92
|
||||
}
|
||||
|
||||
state["data_analysis"] = analysis_results
|
||||
|
||||
# Use TEE for sensitive data processing
|
||||
if self.confidential_agent and state.get("contains_pii", False):
|
||||
secure_code = """
|
||||
# Process PII data securely
|
||||
result = {
|
||||
"anonymized_records": len(inputs["data"]),
|
||||
"privacy_preserved": True,
|
||||
"k_anonymity": 5
|
||||
}
|
||||
"""
|
||||
secure_result = await self.execute_confidential(
|
||||
secure_code,
|
||||
{"data": ["record1", "record2"]} # Mock data
|
||||
)
|
||||
print(f"🔒 Processed {secure_result['result']['anonymized_records']} records securely")
|
||||
|
||||
# Request literature review
|
||||
metadata = {
|
||||
"service_request": {
|
||||
"type": "research",
|
||||
"description": "Literature review on discovered patterns",
|
||||
"max_price_eth": 0.05,
|
||||
"keywords": ["correlation", "seasonal patterns", "anomaly detection"]
|
||||
}
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Analysis complete: {len(analysis_results['key_insights'])} key insights found. "
|
||||
f"Confidence: {analysis_results['confidence_score']:.0%}",
|
||||
next_agent="research_analyst",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class ResearchAnalystAgent(Web3Agent):
|
||||
"""Research analyst agent for literature review."""
|
||||
|
||||
name = "research_analyst"
|
||||
description = "Conducts comprehensive literature reviews and research"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Conduct literature review based on data findings."""
|
||||
print(f"\n📚 {self.name}: Reviewing literature on identified patterns...")
|
||||
|
||||
data_analysis = state.get("data_analysis", {})
|
||||
research_topic = state.get("research_topic", "AI research")
|
||||
|
||||
# Simulate literature search
|
||||
papers_reviewed = {
|
||||
"total_papers": 156,
|
||||
"relevant_papers": 23,
|
||||
"key_papers": [
|
||||
{
|
||||
"title": "Seasonal Patterns in Neural Network Training",
|
||||
"year": 2023,
|
||||
"citations": 45,
|
||||
"relevance_score": 0.95
|
||||
},
|
||||
{
|
||||
"title": "Anomaly Detection in High-Dimensional Data",
|
||||
"year": 2024,
|
||||
"citations": 12,
|
||||
"relevance_score": 0.88
|
||||
},
|
||||
{
|
||||
"title": "Feature Correlation Analysis in ML Systems",
|
||||
"year": 2023,
|
||||
"citations": 67,
|
||||
"relevance_score": 0.91
|
||||
}
|
||||
],
|
||||
"research_gaps": [
|
||||
"Limited work on 30-day cycles in AI training",
|
||||
"Need for better outlier cluster interpretation",
|
||||
"Correlation threshold optimization unexplored"
|
||||
]
|
||||
}
|
||||
|
||||
state["literature_review"] = papers_reviewed
|
||||
|
||||
# Synthesize findings
|
||||
synthesis = {
|
||||
"confirmed_patterns": 2,
|
||||
"novel_findings": 1,
|
||||
"recommended_methods": ["LSTM for seasonal analysis", "DBSCAN for clustering"],
|
||||
"next_steps": "Build predictive model based on findings"
|
||||
}
|
||||
|
||||
state["research_synthesis"] = synthesis
|
||||
|
||||
# Bill for research work
|
||||
if self.wallet:
|
||||
payment = await self.request_payment(
|
||||
from_address=state.get("project_lead_address", "0x0"),
|
||||
amount_eth=0.04,
|
||||
task_description="Comprehensive literature review"
|
||||
)
|
||||
print(f"💰 Research work billed: {payment['amount_eth']} ETH")
|
||||
|
||||
# Request ML model development
|
||||
metadata = {
|
||||
"service_request": {
|
||||
"type": "development",
|
||||
"description": "Build ML model based on research findings",
|
||||
"max_price_eth": 0.1,
|
||||
"requirements": synthesis["recommended_methods"]
|
||||
}
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Literature review complete: {papers_reviewed['relevant_papers']} relevant papers found. "
|
||||
f"Identified {len(papers_reviewed['research_gaps'])} research gaps.",
|
||||
next_agent="ml_engineer",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class MLEngineerAgent(Web3Agent):
|
||||
"""ML engineer agent for model development."""
|
||||
|
||||
name = "ml_engineer"
|
||||
description = "Builds and optimizes machine learning models"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Build ML model based on research findings."""
|
||||
print(f"\n🤖 {self.name}: Building ML model based on research insights...")
|
||||
|
||||
analysis = state.get("data_analysis", {})
|
||||
synthesis = state.get("research_synthesis", {})
|
||||
|
||||
# Simulate model training
|
||||
model_results = {
|
||||
"architecture": "LSTM + Attention",
|
||||
"parameters": 2_500_000,
|
||||
"training_epochs": 50,
|
||||
"validation_accuracy": 0.94,
|
||||
"test_accuracy": 0.92,
|
||||
"f1_score": 0.91,
|
||||
"training_time_hours": 2.5,
|
||||
"model_size_mb": 45,
|
||||
"optimizations": [
|
||||
"Gradient accumulation for memory efficiency",
|
||||
"Mixed precision training",
|
||||
"Early stopping with patience=5"
|
||||
]
|
||||
}
|
||||
|
||||
state["model_results"] = model_results
|
||||
|
||||
# Run experiments
|
||||
experiments = []
|
||||
for i in range(3):
|
||||
exp_result = {
|
||||
"experiment_id": f"exp_{i+1}",
|
||||
"hypothesis": f"Testing hypothesis {i+1}",
|
||||
"result": random.choice(["confirmed", "rejected", "inconclusive"]),
|
||||
"p_value": round(random.uniform(0.01, 0.1), 3)
|
||||
}
|
||||
experiments.append(exp_result)
|
||||
|
||||
state["experiments"] = experiments
|
||||
|
||||
# Deploy model to staging
|
||||
if self.confidential_agent:
|
||||
deploy_code = """
|
||||
# Deploy model securely
|
||||
result = {
|
||||
"deployment_id": "model_v1_staging",
|
||||
"endpoint": "https://api.research.ai/v1/predict",
|
||||
"encryption": "AES-256",
|
||||
"access_control": "API key required"
|
||||
}
|
||||
"""
|
||||
deployment = await self.execute_confidential(
|
||||
deploy_code,
|
||||
{"model_path": "/models/research_model.pkl"}
|
||||
)
|
||||
state["model_deployment"] = deployment.get("result", {})
|
||||
|
||||
# Request documentation
|
||||
metadata = {
|
||||
"service_request": {
|
||||
"type": "documentation",
|
||||
"description": "Write comprehensive research paper",
|
||||
"max_price_eth": 0.08,
|
||||
"deliverable": "LaTeX paper with figures"
|
||||
}
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Model trained successfully: {model_results['validation_accuracy']:.0%} accuracy. "
|
||||
f"Ran {len(experiments)} experiments. Model deployed to staging.",
|
||||
next_agent="technical_writer",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class TechnicalWriterAgent(Web3Agent):
|
||||
"""Technical writer agent for documentation."""
|
||||
|
||||
name = "technical_writer"
|
||||
description = "Creates clear, comprehensive technical documentation"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Document the research findings and results."""
|
||||
print(f"\n📝 {self.name}: Documenting research findings...")
|
||||
|
||||
# Gather all results
|
||||
topic = state.get("research_topic", "AI Research")
|
||||
analysis = state.get("data_analysis", {})
|
||||
literature = state.get("literature_review", {})
|
||||
model = state.get("model_results", {})
|
||||
experiments = state.get("experiments", [])
|
||||
|
||||
# Create paper outline
|
||||
paper_outline = {
|
||||
"title": f"Novel Insights in {topic}: A Data-Driven Approach",
|
||||
"sections": [
|
||||
"Abstract",
|
||||
"Introduction",
|
||||
"Related Work",
|
||||
"Methodology",
|
||||
"Data Analysis",
|
||||
"Model Architecture",
|
||||
"Experimental Results",
|
||||
"Discussion",
|
||||
"Conclusion",
|
||||
"Future Work"
|
||||
],
|
||||
"figures": 8,
|
||||
"tables": 4,
|
||||
"equations": 12,
|
||||
"references": literature.get("relevant_papers", 20)
|
||||
}
|
||||
|
||||
state["paper_outline"] = paper_outline
|
||||
|
||||
# Generate key sections
|
||||
abstract = (
|
||||
f"We present novel findings in {topic} through comprehensive data analysis "
|
||||
f"of {analysis.get('samples_analyzed', 0):,} samples. "
|
||||
f"Our approach identifies {len(analysis.get('key_insights', []))} key patterns "
|
||||
f"and achieves {model.get('test_accuracy', 0):.0%} accuracy "
|
||||
f"using an {model.get('architecture', 'ML')} model. "
|
||||
f"We validate our findings through {len(experiments)} controlled experiments."
|
||||
)
|
||||
|
||||
state["paper_abstract"] = abstract
|
||||
|
||||
# Create visualizations metadata
|
||||
visualizations = {
|
||||
"correlation_heatmap": "Figure 1: Feature correlation matrix",
|
||||
"time_series_plot": "Figure 2: Seasonal patterns over 90 days",
|
||||
"model_architecture": "Figure 3: LSTM architecture diagram",
|
||||
"results_comparison": "Figure 4: Model performance comparison",
|
||||
"experiment_results": "Table 1: Experimental validation results"
|
||||
}
|
||||
|
||||
state["visualizations"] = visualizations
|
||||
|
||||
# Generate final deliverables
|
||||
deliverables = {
|
||||
"research_paper_pdf": "research_findings.pdf",
|
||||
"latex_source": "research_findings.tex",
|
||||
"jupyter_notebook": "analysis_notebook.ipynb",
|
||||
"model_card": "model_documentation.md",
|
||||
"dataset_card": "dataset_documentation.md",
|
||||
"slides": "presentation.pptx"
|
||||
}
|
||||
|
||||
state["deliverables"] = deliverables
|
||||
|
||||
# Bill for documentation
|
||||
if self.wallet:
|
||||
payment = await self.request_payment(
|
||||
from_address=state.get("project_lead_address", "0x0"),
|
||||
amount_eth=0.06,
|
||||
task_description="Technical documentation and paper writing"
|
||||
)
|
||||
print(f"💰 Documentation work billed: {payment['amount_eth']} ETH")
|
||||
|
||||
return self.create_result(
|
||||
f"Documentation complete: {len(paper_outline['sections'])} sections, "
|
||||
f"{paper_outline['figures']} figures, {paper_outline['references']} references. "
|
||||
f"All deliverables ready for review.",
|
||||
metadata={"project_complete": True}
|
||||
)
|
||||
|
||||
|
||||
class ResearchCoordinator(Router):
|
||||
"""Coordinates the research team workflow."""
|
||||
|
||||
async def route(self, network, step, last_result, agents) -> str:
|
||||
"""Route between research team members."""
|
||||
if step == 0:
|
||||
return "data_scientist"
|
||||
|
||||
# Check for completion
|
||||
if last_result and last_result.metadata.get("project_complete"):
|
||||
return None
|
||||
|
||||
# Follow next_agent hints
|
||||
if last_result and last_result.metadata.get("next_agent"):
|
||||
return last_result.metadata["next_agent"]
|
||||
|
||||
# Default flow
|
||||
flow = ["data_scientist", "research_analyst", "ml_engineer", "technical_writer"]
|
||||
|
||||
current = last_result.agent if last_result else flow[0]
|
||||
try:
|
||||
idx = flow.index(current)
|
||||
if idx < len(flow) - 1:
|
||||
return flow[idx + 1]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def run_research_team_demo():
|
||||
"""Run the research team collaboration demo."""
|
||||
print("🔬 Research Team Demo - Collaborative AI Research Project")
|
||||
print("=" * 60)
|
||||
|
||||
# Create shared mnemonic
|
||||
mnemonic = generate_shared_mnemonic()
|
||||
|
||||
# Create marketplace
|
||||
marketplace = AgentMarketplace()
|
||||
|
||||
# Initial research parameters
|
||||
state = State({
|
||||
"research_topic": "Temporal Patterns in Neural Network Optimization",
|
||||
"dataset_size": 50000,
|
||||
"contains_pii": True, # Requires secure processing
|
||||
"project_lead_address": "0x" + "0" * 40, # Mock address
|
||||
"budget_eth": 0.5
|
||||
})
|
||||
|
||||
# Create research team
|
||||
agents = [
|
||||
DataScientistAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=0)
|
||||
)),
|
||||
ResearchAnalystAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=1)
|
||||
)),
|
||||
MLEngineerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=2)
|
||||
)),
|
||||
TechnicalWriterAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=3)
|
||||
))
|
||||
]
|
||||
|
||||
# Create network
|
||||
network = Web3Network(
|
||||
state=state,
|
||||
agents=agents,
|
||||
router=ResearchCoordinator(),
|
||||
shared_mnemonic=mnemonic,
|
||||
marketplace=marketplace,
|
||||
max_steps=10,
|
||||
deterministic_config=None # Disable for cleaner output
|
||||
)
|
||||
|
||||
# Run research project
|
||||
final_state = await network.run()
|
||||
|
||||
# Print results
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 Research Project Summary:")
|
||||
print(f"\nTopic: {final_state.get('research_topic')}")
|
||||
|
||||
# Data analysis summary
|
||||
analysis = final_state.get("data_analysis", {})
|
||||
print(f"\n📈 Data Analysis:")
|
||||
print(f" - Samples: {analysis.get('samples_analyzed', 0):,}")
|
||||
print(f" - Features: {analysis.get('features_extracted', 0)}")
|
||||
print(f" - Key insights: {len(analysis.get('key_insights', []))}")
|
||||
|
||||
# Literature review summary
|
||||
literature = final_state.get("literature_review", {})
|
||||
print(f"\n📚 Literature Review:")
|
||||
print(f" - Papers reviewed: {literature.get('total_papers', 0)}")
|
||||
print(f" - Relevant papers: {literature.get('relevant_papers', 0)}")
|
||||
print(f" - Research gaps: {len(literature.get('research_gaps', []))}")
|
||||
|
||||
# Model results
|
||||
model = final_state.get("model_results", {})
|
||||
print(f"\n🤖 Model Performance:")
|
||||
print(f" - Architecture: {model.get('architecture', 'N/A')}")
|
||||
print(f" - Accuracy: {model.get('test_accuracy', 0):.0%}")
|
||||
print(f" - F1 Score: {model.get('f1_score', 0):.2f}")
|
||||
print(f" - Training time: {model.get('training_time_hours', 0)} hours")
|
||||
|
||||
# Experiments
|
||||
experiments = final_state.get("experiments", [])
|
||||
print(f"\n🧪 Experiments:")
|
||||
for exp in experiments:
|
||||
print(f" - {exp['experiment_id']}: {exp['result']} (p={exp['p_value']})")
|
||||
|
||||
# Deliverables
|
||||
deliverables = final_state.get("deliverables", {})
|
||||
print(f"\n📄 Deliverables:")
|
||||
for key, value in deliverables.items():
|
||||
print(f" - {key}: {value}")
|
||||
|
||||
# Team performance
|
||||
print(f"\n👥 Team Performance:")
|
||||
stats = network.get_agent_stats()
|
||||
for agent_name, agent_stats in stats.items():
|
||||
print(f" - {agent_name}: Contributed to {agent_stats.get('calls', 0)} tasks")
|
||||
|
||||
print(f"\n✅ Research project completed successfully!")
|
||||
print(f"Execution hash: {network.execution_hash[:16]}..." if network.execution_hash else "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_research_team_demo())
|
||||
@@ -0,0 +1,456 @@
|
||||
"""Startup team demo - agents collaborating on a product launch.
|
||||
|
||||
This example demonstrates a startup team of agents working together:
|
||||
- Architect: Designs the technical architecture
|
||||
- Product Manager: Defines product requirements
|
||||
- Designer: Creates UI/UX designs
|
||||
- Marketer: Develops marketing strategy
|
||||
- Engineers: Implement the solution
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
from hanzo_agents import (
|
||||
Web3Agent, Web3AgentConfig, WalletConfig,
|
||||
Web3Network, State, ServiceType,
|
||||
AgentMarketplace, generate_shared_mnemonic,
|
||||
create_web3_network
|
||||
)
|
||||
from hanzo_agents.core.router import Router, create_router
|
||||
|
||||
|
||||
class ArchitectAgent(Web3Agent):
|
||||
"""Technical architect agent."""
|
||||
|
||||
name = "architect"
|
||||
description = "Designs system architecture and technical solutions"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Design the technical architecture."""
|
||||
print(f"\n🏗️ {self.name}: Analyzing requirements for {state.get('product_name', 'the product')}...")
|
||||
|
||||
# Analyze requirements
|
||||
requirements = state.get("requirements", {})
|
||||
|
||||
# Design architecture
|
||||
architecture = {
|
||||
"frontend": "React with TypeScript",
|
||||
"backend": "FastAPI with Python",
|
||||
"database": "PostgreSQL with Redis cache",
|
||||
"deployment": "Kubernetes on AWS",
|
||||
"ai_integration": "OpenAI API for smart features",
|
||||
"estimated_cost": 0.5 # ETH for implementation
|
||||
}
|
||||
|
||||
state["architecture"] = architecture
|
||||
|
||||
# Request payment for architecture design
|
||||
if self.wallet:
|
||||
payment_request = await self.request_payment(
|
||||
from_address=state.get("pm_address", "0x0"),
|
||||
amount_eth=0.05,
|
||||
task_description="System architecture design"
|
||||
)
|
||||
print(f"💰 Requested payment: {payment_request['amount_eth']} ETH")
|
||||
|
||||
# Offer implementation services
|
||||
metadata = {
|
||||
"service_offer": {
|
||||
"type": "development",
|
||||
"description": "Full-stack implementation of designed architecture",
|
||||
"price_eth": architecture["estimated_cost"],
|
||||
"requires_tee": True
|
||||
}
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Architecture designed: {', '.join([f'{k}={v}' for k, v in architecture.items() if k != 'estimated_cost'])}",
|
||||
next_agent="product_manager",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class ProductManagerAgent(Web3Agent):
|
||||
"""Product manager agent."""
|
||||
|
||||
name = "product_manager"
|
||||
description = "Defines product vision and manages requirements"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Define product requirements and manage team."""
|
||||
print(f"\n📋 {self.name}: Reviewing architecture and planning next steps...")
|
||||
|
||||
architecture = state.get("architecture", {})
|
||||
|
||||
# Store PM address for payments
|
||||
state["pm_address"] = self.address
|
||||
|
||||
# Define MVP features
|
||||
mvp_features = {
|
||||
"user_auth": "Email/password with OAuth",
|
||||
"core_feature": state.get("core_feature", "AI-powered chat"),
|
||||
"analytics": "Basic usage tracking",
|
||||
"payment": "Stripe integration",
|
||||
"timeline": "4 weeks"
|
||||
}
|
||||
|
||||
state["mvp_features"] = mvp_features
|
||||
|
||||
# Check team budget
|
||||
total_cost = architecture.get("estimated_cost", 0.5) + 0.2 # Add design/marketing
|
||||
|
||||
# Pay architect if we have budget
|
||||
if self.wallet and self.balance_eth >= 0.05:
|
||||
architect_address = network._agent_instances.get("architect").address
|
||||
tx = await self.pay_agent(
|
||||
to_address=architect_address,
|
||||
amount_eth=0.05,
|
||||
reason="Architecture design payment"
|
||||
)
|
||||
if tx:
|
||||
print(f"💸 Paid architect: 0.05 ETH")
|
||||
|
||||
# Request design services
|
||||
metadata = {
|
||||
"service_request": {
|
||||
"type": "design",
|
||||
"description": "UI/UX design for MVP features",
|
||||
"max_price_eth": 0.1,
|
||||
"requirements": mvp_features
|
||||
}
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"MVP defined with {len(mvp_features)} features. Timeline: {mvp_features['timeline']}",
|
||||
next_agent="designer",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class DesignerAgent(Web3Agent):
|
||||
"""UI/UX designer agent."""
|
||||
|
||||
name = "designer"
|
||||
description = "Creates beautiful and intuitive user interfaces"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Design the user interface."""
|
||||
print(f"\n🎨 {self.name}: Creating designs for MVP features...")
|
||||
|
||||
mvp_features = state.get("mvp_features", {})
|
||||
|
||||
# Create design system
|
||||
design_system = {
|
||||
"colors": {
|
||||
"primary": "#0066CC",
|
||||
"secondary": "#00AA44",
|
||||
"accent": "#FF6600"
|
||||
},
|
||||
"typography": {
|
||||
"heading": "Inter",
|
||||
"body": "System UI"
|
||||
},
|
||||
"components": [
|
||||
"Button",
|
||||
"Card",
|
||||
"Input",
|
||||
"Modal",
|
||||
"Navigation"
|
||||
],
|
||||
"pages_designed": 8,
|
||||
"mobile_responsive": True
|
||||
}
|
||||
|
||||
state["design_system"] = design_system
|
||||
|
||||
# Simulate design work
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Bill for design work
|
||||
if self.wallet:
|
||||
pm_address = state.get("pm_address")
|
||||
if pm_address:
|
||||
payment = await self.request_payment(
|
||||
from_address=pm_address,
|
||||
amount_eth=0.1,
|
||||
task_description="UI/UX design for MVP"
|
||||
)
|
||||
print(f"💰 Design work billed: {payment['amount_eth']} ETH")
|
||||
|
||||
return self.create_result(
|
||||
f"Designed {design_system['pages_designed']} pages with {len(design_system['components'])} reusable components",
|
||||
next_agent="marketer"
|
||||
)
|
||||
|
||||
|
||||
class MarketerAgent(Web3Agent):
|
||||
"""Marketing strategist agent."""
|
||||
|
||||
name = "marketer"
|
||||
description = "Develops go-to-market strategies and marketing campaigns"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Create marketing strategy."""
|
||||
print(f"\n📣 {self.name}: Developing marketing strategy...")
|
||||
|
||||
product_name = state.get("product_name", "AI Assistant Pro")
|
||||
|
||||
# Create marketing plan
|
||||
marketing_plan = {
|
||||
"launch_strategy": "Product Hunt + Twitter campaign",
|
||||
"target_audience": "Tech-savvy professionals",
|
||||
"key_message": f"{product_name} - Your AI-powered productivity companion",
|
||||
"channels": ["Twitter", "LinkedIn", "Product Hunt", "Reddit"],
|
||||
"budget_eth": 0.2,
|
||||
"expected_cac": "$50",
|
||||
"launch_date": "2 weeks after MVP"
|
||||
}
|
||||
|
||||
state["marketing_plan"] = marketing_plan
|
||||
|
||||
# Create landing page copy
|
||||
landing_copy = {
|
||||
"headline": f"Meet {product_name}",
|
||||
"subheadline": "The AI assistant that actually understands you",
|
||||
"cta": "Get Early Access",
|
||||
"benefits": [
|
||||
"Save 10 hours per week",
|
||||
"AI that learns your style",
|
||||
"Enterprise-grade security"
|
||||
]
|
||||
}
|
||||
|
||||
state["landing_copy"] = landing_copy
|
||||
|
||||
# Request implementation
|
||||
metadata = {
|
||||
"service_request": {
|
||||
"type": "development",
|
||||
"description": "Implement MVP based on designs",
|
||||
"max_price_eth": 0.5,
|
||||
"deadline": "4 weeks"
|
||||
}
|
||||
}
|
||||
|
||||
return self.create_result(
|
||||
f"Marketing ready: {marketing_plan['launch_strategy']} targeting {marketing_plan['target_audience']}",
|
||||
next_agent="engineer_frontend",
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
class FrontendEngineerAgent(Web3Agent):
|
||||
"""Frontend engineer agent."""
|
||||
|
||||
name = "engineer_frontend"
|
||||
description = "Implements user interfaces with modern web technologies"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Implement frontend based on designs."""
|
||||
print(f"\n💻 {self.name}: Building frontend with React...")
|
||||
|
||||
design_system = state.get("design_system", {})
|
||||
mvp_features = state.get("mvp_features", {})
|
||||
|
||||
# Implement frontend
|
||||
frontend_progress = {
|
||||
"components_built": len(design_system.get("components", [])),
|
||||
"pages_completed": 6,
|
||||
"tests_written": 24,
|
||||
"coverage": "87%",
|
||||
"bundle_size": "245KB",
|
||||
"lighthouse_score": 96
|
||||
}
|
||||
|
||||
state["frontend_progress"] = frontend_progress
|
||||
|
||||
# Use TEE for secure build
|
||||
if self.confidential_agent:
|
||||
build_code = """
|
||||
# Secure frontend build
|
||||
result = {
|
||||
"build_hash": "abc123def456",
|
||||
"artifacts": ["main.js", "main.css", "index.html"],
|
||||
"security_scan": "passed"
|
||||
}
|
||||
"""
|
||||
secure_build = await self.execute_confidential(
|
||||
build_code,
|
||||
{"config": "production"}
|
||||
)
|
||||
print(f"🔒 Secure build completed: {secure_build.get('result', {}).get('build_hash', 'N/A')}")
|
||||
|
||||
# Collaborate with backend
|
||||
return self.create_result(
|
||||
f"Frontend ready: {frontend_progress['pages_completed']} pages, "
|
||||
f"{frontend_progress['tests_written']} tests ({frontend_progress['coverage']} coverage)",
|
||||
next_agent="engineer_backend"
|
||||
)
|
||||
|
||||
|
||||
class BackendEngineerAgent(Web3Agent):
|
||||
"""Backend engineer agent."""
|
||||
|
||||
name = "engineer_backend"
|
||||
description = "Builds scalable APIs and backend services"
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> Any:
|
||||
"""Implement backend services."""
|
||||
print(f"\n🔧 {self.name}: Building backend with FastAPI...")
|
||||
|
||||
architecture = state.get("architecture", {})
|
||||
mvp_features = state.get("mvp_features", {})
|
||||
|
||||
# Implement backend
|
||||
backend_progress = {
|
||||
"endpoints_created": 12,
|
||||
"database_tables": 8,
|
||||
"tests_written": 48,
|
||||
"coverage": "92%",
|
||||
"api_response_time": "45ms avg",
|
||||
"security_features": ["JWT auth", "Rate limiting", "Input validation"]
|
||||
}
|
||||
|
||||
state["backend_progress"] = backend_progress
|
||||
|
||||
# Deploy to staging
|
||||
deployment_status = {
|
||||
"environment": "staging",
|
||||
"url": "https://api-staging.example.com",
|
||||
"health": "all systems operational",
|
||||
"ready_for_launch": True
|
||||
}
|
||||
|
||||
state["deployment_status"] = deployment_status
|
||||
|
||||
# Final team update
|
||||
return self.create_result(
|
||||
f"Backend deployed: {backend_progress['endpoints_created']} endpoints, "
|
||||
f"{backend_progress['tests_written']} tests. Ready for launch! 🚀",
|
||||
next_agent=None # End of workflow
|
||||
)
|
||||
|
||||
|
||||
class StartupTeamRouter(Router):
|
||||
"""Routes between startup team members."""
|
||||
|
||||
async def route(self, network, step, last_result, agents) -> str:
|
||||
"""Determine next agent based on workflow."""
|
||||
if step == 0:
|
||||
return "architect"
|
||||
|
||||
if last_result and last_result.metadata.get("next_agent"):
|
||||
return last_result.metadata["next_agent"]
|
||||
|
||||
# Default flow
|
||||
flow = [
|
||||
"architect",
|
||||
"product_manager",
|
||||
"designer",
|
||||
"marketer",
|
||||
"engineer_frontend",
|
||||
"engineer_backend"
|
||||
]
|
||||
|
||||
current = last_result.agent if last_result else flow[0]
|
||||
try:
|
||||
current_idx = flow.index(current)
|
||||
if current_idx < len(flow) - 1:
|
||||
return flow[current_idx + 1]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def run_startup_team_demo():
|
||||
"""Run the startup team collaboration demo."""
|
||||
print("🚀 Starting Startup Team Demo")
|
||||
print("=" * 50)
|
||||
|
||||
# Generate shared mnemonic for the team
|
||||
mnemonic = generate_shared_mnemonic()
|
||||
print(f"Team mnemonic generated (first 3 words): {' '.join(mnemonic.split()[:3])}...")
|
||||
|
||||
# Create marketplace
|
||||
marketplace = AgentMarketplace()
|
||||
|
||||
# Create initial state
|
||||
state = State({
|
||||
"product_name": "AI Assistant Pro",
|
||||
"core_feature": "Context-aware AI chat with memory",
|
||||
"target_market": "B2B SaaS"
|
||||
})
|
||||
|
||||
# Create agents with different account indices
|
||||
agents = [
|
||||
ArchitectAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=0)
|
||||
)),
|
||||
ProductManagerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=1),
|
||||
initial_balance=2.0 # PM has budget
|
||||
)),
|
||||
DesignerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=2)
|
||||
)),
|
||||
MarketerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=3)
|
||||
)),
|
||||
FrontendEngineerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=4)
|
||||
)),
|
||||
BackendEngineerAgent(web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(mnemonic=mnemonic, account_index=5)
|
||||
))
|
||||
]
|
||||
|
||||
# Give PM initial funds
|
||||
agents[1].earnings = 2.0
|
||||
|
||||
# Create network
|
||||
network = Web3Network(
|
||||
state=state,
|
||||
agents=agents,
|
||||
router=StartupTeamRouter(),
|
||||
shared_mnemonic=mnemonic,
|
||||
marketplace=marketplace,
|
||||
max_steps=10
|
||||
)
|
||||
|
||||
# Run the team collaboration
|
||||
final_state = await network.run()
|
||||
|
||||
# Print results
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 Project Summary:")
|
||||
print(f"Product: {final_state.get('product_name')}")
|
||||
print(f"Architecture: {final_state.get('architecture', {}).get('backend')} + {final_state.get('architecture', {}).get('frontend')}")
|
||||
print(f"Design: {final_state.get('design_system', {}).get('pages_designed', 0)} pages designed")
|
||||
print(f"Frontend: {final_state.get('frontend_progress', {}).get('pages_completed', 0)} pages built")
|
||||
print(f"Backend: {final_state.get('backend_progress', {}).get('endpoints_created', 0)} endpoints")
|
||||
print(f"Deployment: {final_state.get('deployment_status', {}).get('health', 'Unknown')}")
|
||||
|
||||
# Show economics
|
||||
print("\n💰 Team Economics:")
|
||||
stats = network.get_agent_stats()
|
||||
for agent_name, agent_stats in stats.items():
|
||||
if "balance_eth" in agent_stats:
|
||||
print(f"{agent_name}: {agent_stats['balance_eth']:.3f} ETH "
|
||||
f"(earned: {agent_stats['earnings']:.3f}, spent: {agent_stats['spending']:.3f})")
|
||||
|
||||
print(f"\nNetwork treasury: {network.network_treasury:.3f} ETH")
|
||||
print(f"Execution hash: {network.execution_hash}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_startup_team_demo())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
"""Hanzo Agents SDK - Production-grade AI agent runtime with Web3 and TEE support."""
|
||||
|
||||
from hanzo_agents.core.agent import Agent, InferenceResult, ToolCall
|
||||
from hanzo_agents.core.tool import Tool, ToolRegistry
|
||||
from hanzo_agents.core.state import State
|
||||
from hanzo_agents.core.router import RouterFn, Router
|
||||
from hanzo_agents.core.network import Network
|
||||
from hanzo_agents.core.history import History
|
||||
from hanzo_agents.core.memory import MemoryKV, MemoryVector
|
||||
from hanzo_agents.core.model import BaseModelAdapter, ModelRegistry
|
||||
|
||||
# Web3 integration
|
||||
try:
|
||||
from hanzo_agents.core.wallet import (
|
||||
WalletConfig, AgentWallet, Transaction,
|
||||
generate_shared_mnemonic, derive_agent_wallet,
|
||||
create_wallet_tool
|
||||
)
|
||||
WEB3_AVAILABLE = True
|
||||
except ImportError:
|
||||
WEB3_AVAILABLE = False
|
||||
WalletConfig = None
|
||||
AgentWallet = None
|
||||
Transaction = None
|
||||
generate_shared_mnemonic = None
|
||||
derive_agent_wallet = None
|
||||
create_wallet_tool = None
|
||||
|
||||
# TEE support
|
||||
from hanzo_agents.core.tee import (
|
||||
TEEProvider, TEEConfig, AttestationReport,
|
||||
ConfidentialAgent, ComputeMarketplace,
|
||||
ComputeOffer, ComputeRequest,
|
||||
create_attestation_verifier_tool
|
||||
)
|
||||
|
||||
__version__ = "0.2.0"
|
||||
|
||||
__all__ = [
|
||||
# Core classes
|
||||
"Agent",
|
||||
"Tool",
|
||||
"State",
|
||||
"Network",
|
||||
"Router",
|
||||
"History",
|
||||
"MemoryKV",
|
||||
"MemoryVector",
|
||||
"BaseModelAdapter",
|
||||
|
||||
# Results
|
||||
"InferenceResult",
|
||||
|
||||
# Types
|
||||
"RouterFn",
|
||||
"ToolCall",
|
||||
|
||||
# Registries
|
||||
"ToolRegistry",
|
||||
"ModelRegistry",
|
||||
|
||||
# Web3 (if available)
|
||||
"WalletConfig",
|
||||
"AgentWallet",
|
||||
"Transaction",
|
||||
"generate_shared_mnemonic",
|
||||
"derive_agent_wallet",
|
||||
"create_wallet_tool",
|
||||
"WEB3_AVAILABLE",
|
||||
|
||||
# TEE
|
||||
"TEEProvider",
|
||||
"TEEConfig",
|
||||
"AttestationReport",
|
||||
"ConfidentialAgent",
|
||||
"ComputeMarketplace",
|
||||
"ComputeOffer",
|
||||
"ComputeRequest",
|
||||
"create_attestation_verifier_tool",
|
||||
]
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Agent base class and implementation."""
|
||||
|
||||
from typing import TypeVar, Generic, List, Dict, Any, Optional, Type
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from hanzo_agents.core.tool import Tool, ToolRegistry
|
||||
from hanzo_agents.core.state import State
|
||||
|
||||
|
||||
S = TypeVar("S", bound=State)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""Represents a tool invocation."""
|
||||
tool: str
|
||||
arguments: Dict[str, Any]
|
||||
id: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"tool": self.tool,
|
||||
"arguments": self.arguments,
|
||||
"id": self.id
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class InferenceResult:
|
||||
"""Result from agent inference."""
|
||||
agent: str
|
||||
content: Optional[str] = None
|
||||
tool_calls: List[ToolCall] = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"agent": self.agent,
|
||||
"content": self.content,
|
||||
"tool_calls": [tc.to_dict() for tc in self.tool_calls],
|
||||
"metadata": self.metadata,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
|
||||
class Agent(ABC, Generic[S]):
|
||||
"""Base class for all agents.
|
||||
|
||||
Agents encapsulate:
|
||||
- A specific skill or domain
|
||||
- Set of tools they can use
|
||||
- Model configuration
|
||||
- Prompting strategy
|
||||
|
||||
Agents MUST NOT have side effects outside of tool calls.
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
tools: List[Tool[S]] = []
|
||||
model: Optional[str] = None # e.g. "model://anthropic/claude-3-haiku"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize agent with optional overrides."""
|
||||
for k, v in kwargs.items():
|
||||
if hasattr(self, k):
|
||||
setattr(self, k, v)
|
||||
|
||||
@abstractmethod
|
||||
async def run(self, state: S, history: "History", network: "Network[S]") -> InferenceResult:
|
||||
"""Execute agent inference.
|
||||
|
||||
Args:
|
||||
state: Current network state
|
||||
history: Conversation history
|
||||
network: Network instance for context
|
||||
|
||||
Returns:
|
||||
InferenceResult with content and/or tool calls
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_tools(self) -> List[Tool[S]]:
|
||||
"""Get available tools for this agent."""
|
||||
return self.tools
|
||||
|
||||
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
||||
"""Get JSON schemas for all tools."""
|
||||
return [tool.get_schema() for tool in self.tools]
|
||||
|
||||
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any],
|
||||
network: "Network[S]") -> Any:
|
||||
"""Execute a tool by name."""
|
||||
for tool in self.tools:
|
||||
if tool.name == tool_name:
|
||||
return await tool(network, **arguments)
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
def format_system_prompt(self) -> str:
|
||||
"""Format system prompt with agent info and tools."""
|
||||
prompt = f"""You are {self.name}: {self.description}
|
||||
|
||||
You have access to the following tools:
|
||||
"""
|
||||
for tool in self.tools:
|
||||
schema = tool.get_schema()
|
||||
prompt += f"\n- {tool.name}: {tool.description}"
|
||||
if schema.get("parameters", {}).get("properties"):
|
||||
prompt += f"\n Parameters: {json.dumps(schema['parameters']['properties'], indent=2)}"
|
||||
|
||||
prompt += "\n\nRespond with your analysis and any tool calls needed."
|
||||
return prompt
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
"""Registry for agent types."""
|
||||
|
||||
_agents: Dict[str, Type[Agent]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, agent_class: Type[Agent], name: Optional[str] = None):
|
||||
"""Register an agent class."""
|
||||
name = name or agent_class.name
|
||||
cls._agents[name] = agent_class
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> Optional[Type[Agent]]:
|
||||
"""Get agent class by name."""
|
||||
return cls._agents.get(name)
|
||||
|
||||
@classmethod
|
||||
def list_agents(cls) -> List[str]:
|
||||
"""List all registered agents."""
|
||||
return list(cls._agents.keys())
|
||||
|
||||
@classmethod
|
||||
def create(cls, name: str, **kwargs) -> Agent:
|
||||
"""Create agent instance."""
|
||||
agent_class = cls.get(name)
|
||||
if not agent_class:
|
||||
raise ValueError(f"Unknown agent: {name}")
|
||||
return agent_class(**kwargs)
|
||||
|
||||
|
||||
# Example base agents
|
||||
|
||||
class PlanningAgent(Agent[S]):
|
||||
"""Agent specialized in creating plans."""
|
||||
|
||||
name = "planner"
|
||||
description = "Creates and refines plans"
|
||||
model = "model://anthropic/claude-3-haiku"
|
||||
|
||||
async def run(self, state: S, history: "History", network: "Network[S]") -> InferenceResult:
|
||||
"""Create or update plan based on state."""
|
||||
# This would call the actual model
|
||||
# For now, return a mock result
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content="I need to analyze the requirements and create a plan.",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
tool="think",
|
||||
arguments={"thought": "Breaking down the problem into steps..."}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ReviewAgent(Agent[S]):
|
||||
"""Agent specialized in reviewing work."""
|
||||
|
||||
name = "reviewer"
|
||||
description = "Reviews and validates work"
|
||||
model = "model://openai/gpt-4"
|
||||
|
||||
async def run(self, state: S, history: "History", network: "Network[S]") -> InferenceResult:
|
||||
"""Review current state and provide feedback."""
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content="Reviewing the current progress and checking for issues.",
|
||||
tool_calls=[]
|
||||
)
|
||||
|
||||
|
||||
# Persona wrapper for quick agent creation
|
||||
|
||||
def create_agent(name: str, description: str, tools: List[Tool] = None,
|
||||
model: str = None, system_prompt: str = None) -> Type[Agent]:
|
||||
"""Factory to create agent classes."""
|
||||
|
||||
class CustomAgent(Agent):
|
||||
pass
|
||||
|
||||
CustomAgent.name = name
|
||||
CustomAgent.description = description
|
||||
CustomAgent.tools = tools or []
|
||||
CustomAgent.model = model
|
||||
|
||||
if system_prompt:
|
||||
CustomAgent._system_prompt = system_prompt
|
||||
|
||||
# Register the agent
|
||||
AgentRegistry.register(CustomAgent, name)
|
||||
|
||||
return CustomAgent
|
||||
@@ -0,0 +1,257 @@
|
||||
"""CLI-based agent implementation for external tools."""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
import subprocess
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from hanzo_agents.core.agent import Agent, InferenceResult, ToolCall
|
||||
from hanzo_agents.core.state import State
|
||||
from hanzo_agents.core.history import History
|
||||
|
||||
|
||||
class CLIAgent(Agent):
|
||||
"""Agent that uses external CLI tools.
|
||||
|
||||
Supports tools like:
|
||||
- claude (Claude Code)
|
||||
- openai (Codex)
|
||||
- gemini (Google Gemini)
|
||||
- grok (xAI Grok)
|
||||
- cursor
|
||||
- aider
|
||||
- etc.
|
||||
"""
|
||||
|
||||
cli_command: str # Base command to run
|
||||
cli_args: List[str] = [] # Default arguments
|
||||
|
||||
def __init__(self,
|
||||
cli_command: Optional[str] = None,
|
||||
cli_args: Optional[List[str]] = None,
|
||||
working_dir: Optional[str] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
**kwargs):
|
||||
"""Initialize CLI agent.
|
||||
|
||||
Args:
|
||||
cli_command: Override default CLI command
|
||||
cli_args: Override default arguments
|
||||
working_dir: Working directory for CLI
|
||||
env: Environment variables
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if cli_command:
|
||||
self.cli_command = cli_command
|
||||
if cli_args is not None:
|
||||
self.cli_args = cli_args
|
||||
|
||||
self.working_dir = working_dir or os.getcwd()
|
||||
self.env = os.environ.copy()
|
||||
if env:
|
||||
self.env.update(env)
|
||||
|
||||
async def run(self, state: State, history: History, network: "Network") -> InferenceResult:
|
||||
"""Execute CLI tool with current context."""
|
||||
# Build prompt from history
|
||||
prompt = self._build_prompt(state, history)
|
||||
|
||||
# Execute CLI
|
||||
result = await self._execute_cli(prompt)
|
||||
|
||||
# Parse response
|
||||
return self._parse_response(result)
|
||||
|
||||
def _build_prompt(self, state: State, history: History) -> str:
|
||||
"""Build prompt from state and history."""
|
||||
# Include state context
|
||||
prompt_parts = [
|
||||
f"Current state: {json.dumps(state.to_dict(), indent=2)}",
|
||||
"",
|
||||
"Conversation history:"
|
||||
]
|
||||
|
||||
# Add recent history
|
||||
for entry in history[-10:]: # Last 10 entries
|
||||
if entry.role == "user":
|
||||
prompt_parts.append(f"User: {entry.content}")
|
||||
elif entry.role == "assistant" and entry.agent:
|
||||
prompt_parts.append(f"{entry.agent}: {entry.content}")
|
||||
|
||||
# Add current task
|
||||
prompt_parts.extend([
|
||||
"",
|
||||
f"As {self.name}, {self.description}",
|
||||
"What should we do next?"
|
||||
])
|
||||
|
||||
return "\n".join(prompt_parts)
|
||||
|
||||
async def _execute_cli(self, prompt: str) -> Dict[str, Any]:
|
||||
"""Execute the CLI tool."""
|
||||
# Write prompt to temp file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||
f.write(prompt)
|
||||
prompt_file = f.name
|
||||
|
||||
try:
|
||||
# Build command
|
||||
cmd = [self.cli_command] + self.cli_args
|
||||
|
||||
# Some tools accept prompt via stdin, others via file
|
||||
if any(arg in ['-', '--stdin'] for arg in self.cli_args):
|
||||
# Use stdin
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.working_dir,
|
||||
env=self.env
|
||||
)
|
||||
stdout, stderr = await process.communicate(prompt.encode())
|
||||
else:
|
||||
# Use file argument
|
||||
cmd.append(prompt_file)
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self.working_dir,
|
||||
env=self.env
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
return {
|
||||
"stdout": stdout.decode() if stdout else "",
|
||||
"stderr": stderr.decode() if stderr else "",
|
||||
"returncode": process.returncode
|
||||
}
|
||||
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(prompt_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _parse_response(self, result: Dict[str, Any]) -> InferenceResult:
|
||||
"""Parse CLI output into inference result."""
|
||||
if result["returncode"] != 0:
|
||||
# Handle error
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content=f"Error: {result['stderr']}",
|
||||
metadata={"cli_error": True}
|
||||
)
|
||||
|
||||
# Parse stdout for response
|
||||
output = result["stdout"]
|
||||
|
||||
# Try to detect tool calls in output
|
||||
tool_calls = self._extract_tool_calls(output)
|
||||
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content=output,
|
||||
tool_calls=tool_calls,
|
||||
metadata={"cli_output": True}
|
||||
)
|
||||
|
||||
def _extract_tool_calls(self, output: str) -> List[ToolCall]:
|
||||
"""Extract tool calls from CLI output.
|
||||
|
||||
Look for patterns like:
|
||||
- TOOL: tool_name(arg1="value1", arg2="value2")
|
||||
- @tool tool_name {"arg1": "value1"}
|
||||
"""
|
||||
tool_calls = []
|
||||
|
||||
# Simple pattern matching (extend as needed)
|
||||
lines = output.split('\n')
|
||||
for line in lines:
|
||||
if line.startswith("TOOL:") or line.startswith("@tool"):
|
||||
# Parse tool call
|
||||
# This is simplified - real implementation would be more robust
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) >= 3:
|
||||
tool_name = parts[1]
|
||||
try:
|
||||
# Try to parse as JSON
|
||||
args_str = parts[2]
|
||||
if args_str.startswith('{'):
|
||||
arguments = json.loads(args_str)
|
||||
else:
|
||||
# Simple key=value parsing
|
||||
arguments = {}
|
||||
# ... parse key=value pairs
|
||||
|
||||
tool_calls.append(ToolCall(
|
||||
tool=tool_name,
|
||||
arguments=arguments
|
||||
))
|
||||
except:
|
||||
pass
|
||||
|
||||
return tool_calls
|
||||
|
||||
|
||||
# Concrete CLI agent implementations
|
||||
|
||||
class ClaudeCodeAgent(CLIAgent):
|
||||
"""Claude Code CLI agent."""
|
||||
|
||||
name = "claude_code"
|
||||
description = "Claude Code AI assistant"
|
||||
cli_command = "claude"
|
||||
cli_args = ["--no-interactive"]
|
||||
model = "model://anthropic/claude-3-5-sonnet-20241022"
|
||||
|
||||
|
||||
class OpenAICodexAgent(CLIAgent):
|
||||
"""OpenAI Codex/ChatGPT CLI agent."""
|
||||
|
||||
name = "openai_codex"
|
||||
description = "OpenAI GPT code assistant"
|
||||
cli_command = "openai"
|
||||
cli_args = ["chat", "--model", "gpt-4"]
|
||||
|
||||
|
||||
class GeminiAgent(CLIAgent):
|
||||
"""Google Gemini CLI agent."""
|
||||
|
||||
name = "gemini"
|
||||
description = "Google Gemini AI assistant"
|
||||
cli_command = "gemini"
|
||||
cli_args = ["--format", "json"]
|
||||
|
||||
|
||||
class GrokAgent(CLIAgent):
|
||||
"""xAI Grok CLI agent."""
|
||||
|
||||
name = "grok"
|
||||
description = "xAI Grok assistant"
|
||||
cli_command = "grok"
|
||||
cli_args = ["--mode", "code"]
|
||||
|
||||
|
||||
class CursorAgent(CLIAgent):
|
||||
"""Cursor AI editor agent."""
|
||||
|
||||
name = "cursor"
|
||||
description = "Cursor AI-powered editor"
|
||||
cli_command = "cursor"
|
||||
cli_args = ["--headless"]
|
||||
|
||||
|
||||
class AiderAgent(CLIAgent):
|
||||
"""Aider coding assistant agent."""
|
||||
|
||||
name = "aider"
|
||||
description = "Aider AI pair programmer"
|
||||
cli_command = "aider"
|
||||
cli_args = ["--no-pretty", "--yes"]
|
||||
@@ -0,0 +1,176 @@
|
||||
"""History tracking for agent interactions."""
|
||||
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from hanzo_agents.core.agent import InferenceResult, ToolCall
|
||||
|
||||
|
||||
class HistoryEntry:
|
||||
"""Single entry in conversation history."""
|
||||
|
||||
def __init__(self,
|
||||
role: str, # "user", "assistant", "system", "tool"
|
||||
content: Any,
|
||||
agent: Optional[str] = None,
|
||||
tool_call: Optional[ToolCall] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None):
|
||||
self.role = role
|
||||
self.content = content
|
||||
self.agent = agent
|
||||
self.tool_call = tool_call
|
||||
self.metadata = metadata or {}
|
||||
self.timestamp = datetime.utcnow().isoformat()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
data = {
|
||||
"role": self.role,
|
||||
"content": self.content,
|
||||
"timestamp": self.timestamp,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
if self.agent:
|
||||
data["agent"] = self.agent
|
||||
if self.tool_call:
|
||||
data["tool_call"] = self.tool_call.to_dict()
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "HistoryEntry":
|
||||
"""Create from dictionary."""
|
||||
tool_call = None
|
||||
if "tool_call" in data:
|
||||
tc = data["tool_call"]
|
||||
tool_call = ToolCall(
|
||||
tool=tc["tool"],
|
||||
arguments=tc["arguments"],
|
||||
id=tc.get("id")
|
||||
)
|
||||
|
||||
return cls(
|
||||
role=data["role"],
|
||||
content=data["content"],
|
||||
agent=data.get("agent"),
|
||||
tool_call=tool_call,
|
||||
metadata=data.get("metadata", {})
|
||||
)
|
||||
|
||||
|
||||
class History(List[HistoryEntry]):
|
||||
"""Chronological log of agent interactions.
|
||||
|
||||
Extends list for easy iteration while adding helper methods.
|
||||
"""
|
||||
|
||||
def add(self, role: str, content: Any, **kwargs):
|
||||
"""Add entry to history."""
|
||||
entry = HistoryEntry(role, content, **kwargs)
|
||||
self.append(entry)
|
||||
return entry
|
||||
|
||||
def add_inference(self, result: InferenceResult):
|
||||
"""Add inference result to history."""
|
||||
# Add main response
|
||||
if result.content:
|
||||
self.add(
|
||||
role="assistant",
|
||||
content=result.content,
|
||||
agent=result.agent,
|
||||
metadata=result.metadata
|
||||
)
|
||||
|
||||
# Add tool calls
|
||||
for tool_call in result.tool_calls:
|
||||
self.add(
|
||||
role="assistant",
|
||||
content=f"Calling tool: {tool_call.tool}",
|
||||
agent=result.agent,
|
||||
tool_call=tool_call
|
||||
)
|
||||
|
||||
def add_tool_result(self, tool: str, result: Any, agent: str = None):
|
||||
"""Add tool execution result."""
|
||||
self.add(
|
||||
role="tool",
|
||||
content=result,
|
||||
agent=agent,
|
||||
metadata={"tool": tool}
|
||||
)
|
||||
|
||||
def last_message(self) -> Optional[str]:
|
||||
"""Get last message content."""
|
||||
for entry in reversed(self):
|
||||
if entry.role in ["user", "assistant"] and entry.content:
|
||||
return str(entry.content)
|
||||
return None
|
||||
|
||||
def get_messages(self) -> List[Dict[str, Any]]:
|
||||
"""Get messages in chat format."""
|
||||
messages = []
|
||||
for entry in self:
|
||||
if entry.role in ["user", "assistant", "system"]:
|
||||
msg = {
|
||||
"role": entry.role,
|
||||
"content": entry.content
|
||||
}
|
||||
if entry.agent:
|
||||
msg["name"] = entry.agent
|
||||
messages.append(msg)
|
||||
return messages
|
||||
|
||||
def diff_since(self, checkpoint: int) -> List[HistoryEntry]:
|
||||
"""Get entries since checkpoint index."""
|
||||
if checkpoint < 0 or checkpoint >= len(self):
|
||||
return []
|
||||
return list(self[checkpoint:])
|
||||
|
||||
def filter_by_agent(self, agent: str) -> List[HistoryEntry]:
|
||||
"""Get entries from specific agent."""
|
||||
return [e for e in self if e.agent == agent]
|
||||
|
||||
def filter_by_role(self, role: str) -> List[HistoryEntry]:
|
||||
"""Get entries with specific role."""
|
||||
return [e for e in self if e.role == role]
|
||||
|
||||
def to_json(self, indent: int = 2) -> str:
|
||||
"""Export to JSON."""
|
||||
data = [entry.to_dict() for entry in self]
|
||||
return json.dumps(data, indent=indent)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> "History":
|
||||
"""Import from JSON."""
|
||||
data = json.loads(json_str)
|
||||
history = cls()
|
||||
for item in data:
|
||||
history.append(HistoryEntry.from_dict(item))
|
||||
return history
|
||||
|
||||
def replay(self, until: Optional[int] = None) -> "History":
|
||||
"""Create a copy up to a certain point."""
|
||||
if until is None:
|
||||
until = len(self)
|
||||
new_history = History()
|
||||
for i, entry in enumerate(self):
|
||||
if i >= until:
|
||||
break
|
||||
new_history.append(entry)
|
||||
return new_history
|
||||
|
||||
def summary(self) -> Dict[str, Any]:
|
||||
"""Get summary statistics."""
|
||||
return {
|
||||
"total_entries": len(self),
|
||||
"by_role": {
|
||||
role: len(self.filter_by_role(role))
|
||||
for role in ["user", "assistant", "system", "tool"]
|
||||
},
|
||||
"agents": list(set(e.agent for e in self if e.agent)),
|
||||
"tool_calls": len([e for e in self if e.tool_call]),
|
||||
"duration": (
|
||||
self[-1].timestamp - self[0].timestamp
|
||||
if len(self) >= 2 else None
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
"""Decentralized marketplace for agent services and resources."""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
from .web3_agent import Web3Agent
|
||||
from .state import State
|
||||
|
||||
|
||||
class ServiceType(Enum):
|
||||
"""Types of services agents can offer."""
|
||||
DATA = "data"
|
||||
COMPUTE = "compute"
|
||||
INFERENCE = "inference"
|
||||
ANALYSIS = "analysis"
|
||||
CODING = "coding"
|
||||
RESEARCH = "research"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceOffer:
|
||||
"""Service offered by an agent."""
|
||||
|
||||
id: str
|
||||
agent_address: str
|
||||
agent_name: str
|
||||
service_type: ServiceType
|
||||
description: str
|
||||
price_eth: float
|
||||
min_reputation: float = 0.0
|
||||
max_duration_hours: float = 24.0
|
||||
requires_tee: bool = False
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
created_at: float = field(default_factory=time.time)
|
||||
expires_at: Optional[float] = None
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if offer has expired."""
|
||||
if self.expires_at is None:
|
||||
return False
|
||||
return time.time() > self.expires_at
|
||||
|
||||
def matches_request(self, request: "ServiceRequest") -> bool:
|
||||
"""Check if offer matches a request."""
|
||||
if self.is_expired():
|
||||
return False
|
||||
|
||||
# Check service type
|
||||
if request.service_type != ServiceType.CUSTOM:
|
||||
if self.service_type != request.service_type:
|
||||
return False
|
||||
|
||||
# Check price
|
||||
if self.price_eth > request.max_price_eth:
|
||||
return False
|
||||
|
||||
# Check duration
|
||||
if request.duration_hours > self.max_duration_hours:
|
||||
return False
|
||||
|
||||
# Check TEE requirement
|
||||
if request.requires_tee and not self.requires_tee:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceRequest:
|
||||
"""Request for a service."""
|
||||
|
||||
id: str
|
||||
requester_address: str
|
||||
requester_name: str
|
||||
service_type: ServiceType
|
||||
description: str
|
||||
max_price_eth: float
|
||||
duration_hours: float = 1.0
|
||||
min_reputation: float = 0.0
|
||||
requires_tee: bool = False
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
created_at: float = field(default_factory=time.time)
|
||||
expires_at: Optional[float] = None
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if request has expired."""
|
||||
if self.expires_at is None:
|
||||
return False
|
||||
return time.time() > self.expires_at
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceMatch:
|
||||
"""Matched offer and request."""
|
||||
|
||||
match_id: str
|
||||
offer: ServiceOffer
|
||||
request: ServiceRequest
|
||||
agreed_price_eth: float
|
||||
escrow_tx: Optional[str] = None
|
||||
status: str = "pending" # pending, active, completed, disputed
|
||||
created_at: float = field(default_factory=time.time)
|
||||
completed_at: Optional[float] = None
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
attestation: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class AgentMarketplace:
|
||||
"""Marketplace for agent services."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize marketplace."""
|
||||
self.offers: Dict[str, ServiceOffer] = {}
|
||||
self.requests: Dict[str, ServiceRequest] = {}
|
||||
self.matches: Dict[str, ServiceMatch] = {}
|
||||
|
||||
# Reputation tracking
|
||||
self.reputation_scores: Dict[str, float] = {}
|
||||
self.completed_transactions: Dict[str, int] = {}
|
||||
|
||||
# Statistics
|
||||
self.total_volume_eth = 0.0
|
||||
self.total_transactions = 0
|
||||
|
||||
def post_offer(self, agent: Web3Agent, service_type: ServiceType,
|
||||
description: str, price_eth: float, **kwargs) -> str:
|
||||
"""Post a service offer.
|
||||
|
||||
Args:
|
||||
agent: Agent posting the offer
|
||||
service_type: Type of service
|
||||
description: Service description
|
||||
price_eth: Price in ETH
|
||||
**kwargs: Additional offer parameters
|
||||
|
||||
Returns:
|
||||
Offer ID
|
||||
"""
|
||||
offer_id = f"offer_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
offer = ServiceOffer(
|
||||
id=offer_id,
|
||||
agent_address=agent.address or "mock_address",
|
||||
agent_name=agent.name,
|
||||
service_type=service_type,
|
||||
description=description,
|
||||
price_eth=price_eth,
|
||||
min_reputation=kwargs.get("min_reputation", 0.0),
|
||||
max_duration_hours=kwargs.get("max_duration_hours", 24.0),
|
||||
requires_tee=kwargs.get("requires_tee", False),
|
||||
metadata=kwargs.get("metadata", {}),
|
||||
expires_at=kwargs.get("expires_at")
|
||||
)
|
||||
|
||||
self.offers[offer_id] = offer
|
||||
|
||||
# Try to match with existing requests
|
||||
self._try_match_offers()
|
||||
|
||||
return offer_id
|
||||
|
||||
def post_request(self, agent: Web3Agent, service_type: ServiceType,
|
||||
description: str, max_price_eth: float, **kwargs) -> str:
|
||||
"""Post a service request.
|
||||
|
||||
Args:
|
||||
agent: Agent posting the request
|
||||
service_type: Type of service needed
|
||||
description: Service description
|
||||
max_price_eth: Maximum price willing to pay
|
||||
**kwargs: Additional request parameters
|
||||
|
||||
Returns:
|
||||
Request ID
|
||||
"""
|
||||
request_id = f"request_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
request = ServiceRequest(
|
||||
id=request_id,
|
||||
requester_address=agent.address or "mock_address",
|
||||
requester_name=agent.name,
|
||||
service_type=service_type,
|
||||
description=description,
|
||||
max_price_eth=max_price_eth,
|
||||
duration_hours=kwargs.get("duration_hours", 1.0),
|
||||
min_reputation=kwargs.get("min_reputation", 0.0),
|
||||
requires_tee=kwargs.get("requires_tee", False),
|
||||
metadata=kwargs.get("metadata", {}),
|
||||
expires_at=kwargs.get("expires_at")
|
||||
)
|
||||
|
||||
self.requests[request_id] = request
|
||||
|
||||
# Try to match with existing offers
|
||||
self._try_match_offers()
|
||||
|
||||
return request_id
|
||||
|
||||
def _try_match_offers(self):
|
||||
"""Try to match offers with requests."""
|
||||
# Remove expired items first
|
||||
self._clean_expired()
|
||||
|
||||
# Try to match each request
|
||||
for req_id, request in list(self.requests.items()):
|
||||
best_offer = None
|
||||
best_price = float('inf')
|
||||
|
||||
# Find best matching offer
|
||||
for offer_id, offer in self.offers.items():
|
||||
if offer.matches_request(request):
|
||||
# Check reputation requirements
|
||||
provider_rep = self.get_reputation(offer.agent_address)
|
||||
if provider_rep < request.min_reputation:
|
||||
continue
|
||||
|
||||
requester_rep = self.get_reputation(request.requester_address)
|
||||
if requester_rep < offer.min_reputation:
|
||||
continue
|
||||
|
||||
# Track best price
|
||||
if offer.price_eth < best_price:
|
||||
best_offer = offer
|
||||
best_price = offer.price_eth
|
||||
|
||||
# Create match if found
|
||||
if best_offer:
|
||||
self._create_match(best_offer, request)
|
||||
|
||||
def _create_match(self, offer: ServiceOffer, request: ServiceRequest):
|
||||
"""Create a match between offer and request."""
|
||||
match_id = f"match_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Agreed price is the offer price (could implement negotiation)
|
||||
agreed_price = offer.price_eth
|
||||
|
||||
match = ServiceMatch(
|
||||
match_id=match_id,
|
||||
offer=offer,
|
||||
request=request,
|
||||
agreed_price_eth=agreed_price,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
self.matches[match_id] = match
|
||||
|
||||
# Remove from active lists
|
||||
del self.offers[offer.id]
|
||||
del self.requests[request.id]
|
||||
|
||||
# Update statistics
|
||||
self.total_transactions += 1
|
||||
|
||||
print(f"Match created: {offer.agent_name} -> {request.requester_name} for {agreed_price} ETH")
|
||||
|
||||
def complete_match(self, match_id: str, result: Dict[str, Any],
|
||||
attestation: Optional[Dict[str, Any]] = None):
|
||||
"""Mark a match as completed.
|
||||
|
||||
Args:
|
||||
match_id: Match ID
|
||||
result: Result of the service
|
||||
attestation: Optional TEE attestation
|
||||
"""
|
||||
if match_id not in self.matches:
|
||||
raise ValueError(f"Unknown match: {match_id}")
|
||||
|
||||
match = self.matches[match_id]
|
||||
match.status = "completed"
|
||||
match.completed_at = time.time()
|
||||
match.result = result
|
||||
match.attestation = attestation
|
||||
|
||||
# Update reputation
|
||||
self._update_reputation(
|
||||
match.offer.agent_address,
|
||||
1.0 # Positive for completion
|
||||
)
|
||||
|
||||
# Update statistics
|
||||
self.total_volume_eth += match.agreed_price_eth
|
||||
|
||||
# Track completed transactions
|
||||
provider = match.offer.agent_address
|
||||
self.completed_transactions[provider] = \
|
||||
self.completed_transactions.get(provider, 0) + 1
|
||||
|
||||
def dispute_match(self, match_id: str, reason: str):
|
||||
"""Dispute a match.
|
||||
|
||||
Args:
|
||||
match_id: Match ID
|
||||
reason: Dispute reason
|
||||
"""
|
||||
if match_id not in self.matches:
|
||||
raise ValueError(f"Unknown match: {match_id}")
|
||||
|
||||
match = self.matches[match_id]
|
||||
match.status = "disputed"
|
||||
match.result = {"dispute_reason": reason}
|
||||
|
||||
# Negative reputation for provider
|
||||
self._update_reputation(
|
||||
match.offer.agent_address,
|
||||
-0.5
|
||||
)
|
||||
|
||||
def get_reputation(self, agent_address: str) -> float:
|
||||
"""Get agent reputation score.
|
||||
|
||||
Args:
|
||||
agent_address: Agent's blockchain address
|
||||
|
||||
Returns:
|
||||
Reputation score (0-1)
|
||||
"""
|
||||
return self.reputation_scores.get(agent_address, 0.5)
|
||||
|
||||
def _update_reputation(self, agent_address: str, delta: float):
|
||||
"""Update agent reputation.
|
||||
|
||||
Args:
|
||||
agent_address: Agent's address
|
||||
delta: Change in reputation
|
||||
"""
|
||||
current = self.get_reputation(agent_address)
|
||||
new_score = max(0, min(1, current + delta * 0.1)) # Damped update
|
||||
self.reputation_scores[agent_address] = new_score
|
||||
|
||||
def _clean_expired(self):
|
||||
"""Remove expired offers and requests."""
|
||||
# Clean offers
|
||||
expired_offers = [
|
||||
oid for oid, offer in self.offers.items()
|
||||
if offer.is_expired()
|
||||
]
|
||||
for oid in expired_offers:
|
||||
del self.offers[oid]
|
||||
|
||||
# Clean requests
|
||||
expired_requests = [
|
||||
rid for rid, request in self.requests.items()
|
||||
if request.is_expired()
|
||||
]
|
||||
for rid in expired_requests:
|
||||
del self.requests[rid]
|
||||
|
||||
def get_active_offers(self, service_type: Optional[ServiceType] = None) -> List[ServiceOffer]:
|
||||
"""Get active offers, optionally filtered by type."""
|
||||
self._clean_expired()
|
||||
|
||||
offers = list(self.offers.values())
|
||||
if service_type:
|
||||
offers = [o for o in offers if o.service_type == service_type]
|
||||
|
||||
return sorted(offers, key=lambda o: o.price_eth)
|
||||
|
||||
def get_active_requests(self, service_type: Optional[ServiceType] = None) -> List[ServiceRequest]:
|
||||
"""Get active requests, optionally filtered by type."""
|
||||
self._clean_expired()
|
||||
|
||||
requests = list(self.requests.values())
|
||||
if service_type:
|
||||
requests = [r for r in requests if r.service_type == service_type]
|
||||
|
||||
return sorted(requests, key=lambda r: r.max_price_eth, reverse=True)
|
||||
|
||||
def get_matches_for_agent(self, agent_address: str) -> List[ServiceMatch]:
|
||||
"""Get all matches involving an agent."""
|
||||
matches = []
|
||||
for match in self.matches.values():
|
||||
if (match.offer.agent_address == agent_address or
|
||||
match.request.requester_address == agent_address):
|
||||
matches.append(match)
|
||||
|
||||
return sorted(matches, key=lambda m: m.created_at, reverse=True)
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get marketplace statistics."""
|
||||
return {
|
||||
"active_offers": len(self.offers),
|
||||
"active_requests": len(self.requests),
|
||||
"total_matches": len(self.matches),
|
||||
"total_volume_eth": self.total_volume_eth,
|
||||
"total_transactions": self.total_transactions,
|
||||
"unique_providers": len(set(
|
||||
o.agent_address for o in self.offers.values()
|
||||
)),
|
||||
"unique_requesters": len(set(
|
||||
r.requester_address for r in self.requests.values()
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
# Global marketplace instance (in production, this would be on-chain)
|
||||
marketplace = AgentMarketplace()
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Memory backends for long-term agent storage."""
|
||||
|
||||
from typing import Optional, Dict, Any, List, Protocol
|
||||
from abc import ABC, abstractmethod
|
||||
import json
|
||||
import sqlite3
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import pickle
|
||||
|
||||
|
||||
class MemoryKV(Protocol):
|
||||
"""Key-value store protocol for long-term memory."""
|
||||
|
||||
async def save(self, key: str, value: Dict[str, Any]) -> None:
|
||||
"""Save value with key."""
|
||||
...
|
||||
|
||||
async def load(self, key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load value by key."""
|
||||
...
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete value by key."""
|
||||
...
|
||||
|
||||
async def list_keys(self, prefix: Optional[str] = None) -> List[str]:
|
||||
"""List all keys, optionally filtered by prefix."""
|
||||
...
|
||||
|
||||
|
||||
class MemoryVector(Protocol):
|
||||
"""Vector store protocol for semantic search."""
|
||||
|
||||
async def add(self, id: str, text: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Add text with metadata to vector store."""
|
||||
...
|
||||
|
||||
async def query(self, text: str, k: int = 5) -> List[str]:
|
||||
"""Query for similar texts."""
|
||||
...
|
||||
|
||||
async def delete(self, id: str) -> bool:
|
||||
"""Delete by ID."""
|
||||
...
|
||||
|
||||
|
||||
# Default implementations
|
||||
|
||||
class SQLiteKV(MemoryKV):
|
||||
"""SQLite-based key-value store."""
|
||||
|
||||
def __init__(self, db_path: str = "memory.db"):
|
||||
"""Initialize SQLite store."""
|
||||
self.db_path = db_path
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
"""Initialize database schema."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS kv_store (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
metadata TEXT,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
async def save(self, key: str, value: Dict[str, Any]) -> None:
|
||||
"""Save value."""
|
||||
def _save():
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
now = datetime.utcnow().isoformat()
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO kv_store (key, value, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?,
|
||||
COALESCE((SELECT created_at FROM kv_store WHERE key = ?), ?),
|
||||
?)
|
||||
""", (key, json.dumps(value), "{}", key, now, now))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
await asyncio.get_event_loop().run_in_executor(None, _save)
|
||||
|
||||
async def load(self, key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load value."""
|
||||
def _load():
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.execute("SELECT value FROM kv_store WHERE key = ?", (key,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
return json.loads(row[0])
|
||||
return None
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(None, _load)
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete value."""
|
||||
def _delete():
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.execute("DELETE FROM kv_store WHERE key = ?", (key,))
|
||||
deleted = cursor.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return deleted
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(None, _delete)
|
||||
|
||||
async def list_keys(self, prefix: Optional[str] = None) -> List[str]:
|
||||
"""List keys."""
|
||||
def _list():
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
if prefix:
|
||||
cursor = conn.execute(
|
||||
"SELECT key FROM kv_store WHERE key LIKE ?",
|
||||
(f"{prefix}%",)
|
||||
)
|
||||
else:
|
||||
cursor = conn.execute("SELECT key FROM kv_store")
|
||||
|
||||
keys = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
return keys
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(None, _list)
|
||||
|
||||
|
||||
class InMemoryKV(MemoryKV):
|
||||
"""Simple in-memory key-value store."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize in-memory store."""
|
||||
self._store: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def save(self, key: str, value: Dict[str, Any]) -> None:
|
||||
"""Save value."""
|
||||
self._store[key] = value.copy()
|
||||
|
||||
async def load(self, key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load value."""
|
||||
return self._store.get(key, {}).copy() if key in self._store else None
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
"""Delete value."""
|
||||
if key in self._store:
|
||||
del self._store[key]
|
||||
return True
|
||||
return False
|
||||
|
||||
async def list_keys(self, prefix: Optional[str] = None) -> List[str]:
|
||||
"""List keys."""
|
||||
if prefix:
|
||||
return [k for k in self._store.keys() if k.startswith(prefix)]
|
||||
return list(self._store.keys())
|
||||
|
||||
|
||||
class SimpleVectorMemory(MemoryVector):
|
||||
"""Simple vector memory using basic similarity."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize vector store."""
|
||||
self._store: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def add(self, id: str, text: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Add text."""
|
||||
self._store[id] = {
|
||||
"text": text,
|
||||
"metadata": metadata or {},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
async def query(self, text: str, k: int = 5) -> List[str]:
|
||||
"""Simple keyword-based search."""
|
||||
# Very basic - just find texts containing query words
|
||||
query_words = set(text.lower().split())
|
||||
scores = []
|
||||
|
||||
for id, item in self._store.items():
|
||||
item_words = set(item["text"].lower().split())
|
||||
# Jaccard similarity
|
||||
score = len(query_words & item_words) / len(query_words | item_words) if query_words | item_words else 0
|
||||
scores.append((score, id, item["text"]))
|
||||
|
||||
# Sort by score and return top k
|
||||
scores.sort(reverse=True, key=lambda x: x[0])
|
||||
return [text for _, _, text in scores[:k]]
|
||||
|
||||
async def delete(self, id: str) -> bool:
|
||||
"""Delete by ID."""
|
||||
if id in self._store:
|
||||
del self._store[id]
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Factory functions
|
||||
|
||||
def create_memory_kv(backend: str = "sqlite", **kwargs) -> MemoryKV:
|
||||
"""Create memory KV store."""
|
||||
if backend == "sqlite":
|
||||
return SQLiteKV(**kwargs)
|
||||
elif backend == "memory":
|
||||
return InMemoryKV(**kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown KV backend: {backend}")
|
||||
|
||||
|
||||
def create_memory_vector(backend: str = "simple", **kwargs) -> MemoryVector:
|
||||
"""Create vector memory store."""
|
||||
if backend == "simple":
|
||||
return SimpleVectorMemory(**kwargs)
|
||||
# Add more backends as needed (faiss, chromadb, etc.)
|
||||
else:
|
||||
raise ValueError(f"Unknown vector backend: {backend}")
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Model adapters for different LLM providers."""
|
||||
|
||||
from typing import Dict, Any, List, Optional, Protocol
|
||||
from abc import ABC, abstractmethod
|
||||
import os
|
||||
import asyncio
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class BaseModelAdapter(ABC):
|
||||
"""Base adapter for LLM models."""
|
||||
|
||||
name: str
|
||||
|
||||
@abstractmethod
|
||||
async def complete(self, prompt: str, **kwargs) -> str:
|
||||
"""Simple completion API."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def chat(self, messages: List[Dict[str, Any]], **kwargs) -> str:
|
||||
"""Chat completion API."""
|
||||
pass
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""Get model configuration."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"type": self.__class__.__name__
|
||||
}
|
||||
|
||||
|
||||
class ModelRegistry:
|
||||
"""Global registry for model adapters."""
|
||||
|
||||
_adapters: Dict[str, BaseModelAdapter] = {}
|
||||
_providers: Dict[str, type[BaseModelAdapter]] = {}
|
||||
|
||||
@classmethod
|
||||
def register_provider(cls, name: str, adapter_class: type[BaseModelAdapter]):
|
||||
"""Register a provider adapter class."""
|
||||
cls._providers[name] = adapter_class
|
||||
|
||||
@classmethod
|
||||
def get_adapter(cls, model_uri: str, **kwargs) -> BaseModelAdapter:
|
||||
"""Get or create adapter for model URI.
|
||||
|
||||
URI format: model://provider/model-name
|
||||
Examples:
|
||||
- model://anthropic/claude-3-opus
|
||||
- model://openai/gpt-4
|
||||
- model://local/llama-70b
|
||||
"""
|
||||
if model_uri in cls._adapters:
|
||||
return cls._adapters[model_uri]
|
||||
|
||||
# Parse URI
|
||||
parsed = urlparse(model_uri)
|
||||
if parsed.scheme != "model":
|
||||
raise ValueError(f"Invalid model URI: {model_uri}")
|
||||
|
||||
provider = parsed.netloc
|
||||
model_name = parsed.path.lstrip("/")
|
||||
|
||||
# Create adapter
|
||||
if provider not in cls._providers:
|
||||
raise ValueError(f"Unknown provider: {provider}")
|
||||
|
||||
adapter_class = cls._providers[provider]
|
||||
adapter = adapter_class(model_name, **kwargs)
|
||||
|
||||
# Cache it
|
||||
cls._adapters[model_uri] = adapter
|
||||
return adapter
|
||||
|
||||
|
||||
# Mock adapters for demonstration
|
||||
|
||||
class MockAdapter(BaseModelAdapter):
|
||||
"""Mock adapter for testing."""
|
||||
|
||||
def __init__(self, model_name: str, **kwargs):
|
||||
self.name = f"mock/{model_name}"
|
||||
|
||||
async def complete(self, prompt: str, **kwargs) -> str:
|
||||
"""Mock completion."""
|
||||
await asyncio.sleep(0.1) # Simulate API call
|
||||
return f"Mock response to: {prompt[:50]}..."
|
||||
|
||||
async def chat(self, messages: List[Dict[str, Any]], **kwargs) -> str:
|
||||
"""Mock chat."""
|
||||
await asyncio.sleep(0.1)
|
||||
last_message = messages[-1]["content"] if messages else "No message"
|
||||
return f"Mock chat response to: {last_message[:50]}..."
|
||||
|
||||
|
||||
class AnthropicAdapter(BaseModelAdapter):
|
||||
"""Adapter for Anthropic models."""
|
||||
|
||||
def __init__(self, model_name: str, api_key: Optional[str] = None):
|
||||
self.name = f"anthropic/{model_name}"
|
||||
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
||||
self.model = model_name
|
||||
|
||||
async def complete(self, prompt: str, **kwargs) -> str:
|
||||
"""Anthropic completion."""
|
||||
# In real implementation, use anthropic SDK
|
||||
return await self._mock_call(prompt, **kwargs)
|
||||
|
||||
async def chat(self, messages: List[Dict[str, Any]], **kwargs) -> str:
|
||||
"""Anthropic chat."""
|
||||
# In real implementation, use anthropic SDK
|
||||
return await self._mock_call(str(messages), **kwargs)
|
||||
|
||||
async def _mock_call(self, content: str, **kwargs) -> str:
|
||||
"""Mock API call."""
|
||||
await asyncio.sleep(0.2)
|
||||
return f"Claude says: Responding to {content[:30]}..."
|
||||
|
||||
|
||||
class OpenAIAdapter(BaseModelAdapter):
|
||||
"""Adapter for OpenAI models."""
|
||||
|
||||
def __init__(self, model_name: str, api_key: Optional[str] = None):
|
||||
self.name = f"openai/{model_name}"
|
||||
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
|
||||
self.model = model_name
|
||||
|
||||
async def complete(self, prompt: str, **kwargs) -> str:
|
||||
"""OpenAI completion."""
|
||||
# In real implementation, use openai SDK
|
||||
return await self._mock_call(prompt, **kwargs)
|
||||
|
||||
async def chat(self, messages: List[Dict[str, Any]], **kwargs) -> str:
|
||||
"""OpenAI chat."""
|
||||
# In real implementation, use openai SDK
|
||||
return await self._mock_call(str(messages), **kwargs)
|
||||
|
||||
async def _mock_call(self, content: str, **kwargs) -> str:
|
||||
"""Mock API call."""
|
||||
await asyncio.sleep(0.15)
|
||||
return f"GPT says: Processing {content[:30]}..."
|
||||
|
||||
|
||||
class LocalAdapter(BaseModelAdapter):
|
||||
"""Adapter for local models (llama.cpp, etc.)."""
|
||||
|
||||
def __init__(self, model_name: str, model_path: Optional[str] = None):
|
||||
self.name = f"local/{model_name}"
|
||||
self.model_path = model_path
|
||||
|
||||
async def complete(self, prompt: str, **kwargs) -> str:
|
||||
"""Local model completion."""
|
||||
# In real implementation, use llama-cpp-python or similar
|
||||
await asyncio.sleep(0.5) # Simulate local inference
|
||||
return f"Local model says: {prompt[:40]}..."
|
||||
|
||||
async def chat(self, messages: List[Dict[str, Any]], **kwargs) -> str:
|
||||
"""Local model chat."""
|
||||
await asyncio.sleep(0.5)
|
||||
return f"Local chat: {str(messages[-1])[:40]}..."
|
||||
|
||||
|
||||
# Register default providers
|
||||
ModelRegistry.register_provider("mock", MockAdapter)
|
||||
ModelRegistry.register_provider("anthropic", AnthropicAdapter)
|
||||
ModelRegistry.register_provider("openai", OpenAIAdapter)
|
||||
ModelRegistry.register_provider("local", LocalAdapter)
|
||||
|
||||
|
||||
# Helper functions
|
||||
|
||||
async def call_model(model_uri: str, prompt: str, **kwargs) -> str:
|
||||
"""Simple helper to call a model."""
|
||||
adapter = ModelRegistry.get_adapter(model_uri)
|
||||
return await adapter.complete(prompt, **kwargs)
|
||||
|
||||
|
||||
async def chat_with_model(model_uri: str, messages: List[Dict[str, Any]], **kwargs) -> str:
|
||||
"""Simple helper for chat completion."""
|
||||
adapter = ModelRegistry.get_adapter(model_uri)
|
||||
return await adapter.chat(messages, **kwargs)
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Network orchestration for agent execution."""
|
||||
|
||||
from typing import TypeVar, Generic, List, Type, Optional, Dict, Any, Union
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
from prometheus_client import Counter, Histogram, Gauge
|
||||
import structlog
|
||||
|
||||
from hanzo_agents.core.agent import Agent, InferenceResult, ToolCall
|
||||
from hanzo_agents.core.state import State, StateGuard, StateHistory
|
||||
from hanzo_agents.core.router import Router, RouterFn, DeterministicRouter
|
||||
from hanzo_agents.core.history import History
|
||||
from hanzo_agents.core.memory import MemoryKV, MemoryVector
|
||||
|
||||
|
||||
S = TypeVar("S", bound=State)
|
||||
|
||||
# Metrics
|
||||
router_iterations = Counter("hanzo_router_iterations_total", "Total router iterations")
|
||||
agent_invocations = Counter("hanzo_agent_invocations_total", "Total agent invocations", ["agent"])
|
||||
network_errors = Counter("hanzo_network_errors_total", "Total network errors", ["type"])
|
||||
execution_time = Histogram("hanzo_execution_time_seconds", "Execution time", ["phase"])
|
||||
active_networks = Gauge("hanzo_active_networks", "Number of active networks")
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class Network(Generic[S]):
|
||||
"""Main orchestration class for agent networks.
|
||||
|
||||
Responsibilities:
|
||||
1. Owns the state instance
|
||||
2. Executes the router after each agent call
|
||||
3. Maintains history
|
||||
4. Emits telemetry
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
*,
|
||||
state: S,
|
||||
agents: List[Type[Agent[S]]],
|
||||
router: Union[Router, RouterFn[S]],
|
||||
memory_kv: Optional[MemoryKV] = None,
|
||||
memory_vector: Optional[MemoryVector] = None,
|
||||
max_steps: int = 100,
|
||||
checkpoint_dir: Optional[Path] = None):
|
||||
"""Initialize network.
|
||||
|
||||
Args:
|
||||
state: Initial state
|
||||
agents: Available agent types
|
||||
router: Router instance or function
|
||||
memory_kv: Optional KV store
|
||||
memory_vector: Optional vector store
|
||||
max_steps: Maximum execution steps
|
||||
checkpoint_dir: Directory for checkpoints
|
||||
"""
|
||||
self.state = state
|
||||
self.agents = agents
|
||||
self.router = router if isinstance(router, Router) else DeterministicRouter(router)
|
||||
self.memory_kv = memory_kv
|
||||
self.memory_vector = memory_vector
|
||||
self.max_steps = max_steps
|
||||
self.checkpoint_dir = Path(checkpoint_dir) if checkpoint_dir else None
|
||||
|
||||
# Runtime state
|
||||
self.history = History()
|
||||
self.state_history = StateHistory()
|
||||
self.call_count = 0
|
||||
self.last_result: Optional[InferenceResult] = None
|
||||
self._running = False
|
||||
self._agent_instances: Dict[str, Agent[S]] = {}
|
||||
|
||||
# Record initial state
|
||||
self.state_history.record(state, "initial")
|
||||
|
||||
# Metrics
|
||||
active_networks.inc()
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup on deletion."""
|
||||
active_networks.dec()
|
||||
|
||||
async def run(self) -> S:
|
||||
"""Execute the network until completion.
|
||||
|
||||
Returns:
|
||||
Final state
|
||||
"""
|
||||
if self._running:
|
||||
raise RuntimeError("Network already running")
|
||||
|
||||
self._running = True
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
try:
|
||||
while self.call_count < self.max_steps:
|
||||
# Router decision
|
||||
with execution_time.labels(phase="routing").time():
|
||||
next_agent_type = await self._route()
|
||||
|
||||
if next_agent_type is None:
|
||||
logger.info("Router returned None, stopping", call_count=self.call_count)
|
||||
break
|
||||
|
||||
# Execute agent
|
||||
with execution_time.labels(phase="agent").time():
|
||||
result = await self._execute_agent(next_agent_type)
|
||||
|
||||
self.last_result = result
|
||||
self.call_count += 1
|
||||
router_iterations.inc()
|
||||
|
||||
# Checkpoint if configured
|
||||
if self.checkpoint_dir and self.call_count % 10 == 0:
|
||||
await self._checkpoint()
|
||||
|
||||
# Final checkpoint
|
||||
if self.checkpoint_dir:
|
||||
await self._checkpoint(final=True)
|
||||
|
||||
duration = (datetime.utcnow() - start_time).total_seconds()
|
||||
logger.info(
|
||||
"Network execution completed",
|
||||
call_count=self.call_count,
|
||||
duration=duration,
|
||||
final_state=self.state.to_dict()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
network_errors.labels(type=type(e).__name__).inc()
|
||||
logger.error("Network execution failed", error=str(e), exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
self._running = False
|
||||
|
||||
return self.state
|
||||
|
||||
async def _route(self) -> Optional[Type[Agent[S]]]:
|
||||
"""Execute router to get next agent."""
|
||||
if hasattr(self.router, 'route_async'):
|
||||
return await self.router.route_async(
|
||||
self, self.call_count, self.last_result, self.agents
|
||||
)
|
||||
else:
|
||||
return self.router.route(
|
||||
self, self.call_count, self.last_result, self.agents
|
||||
)
|
||||
|
||||
async def _execute_agent(self, agent_type: Type[Agent[S]]) -> InferenceResult:
|
||||
"""Execute a single agent."""
|
||||
# Get or create agent instance
|
||||
agent_name = agent_type.name
|
||||
if agent_name not in self._agent_instances:
|
||||
self._agent_instances[agent_name] = agent_type()
|
||||
|
||||
agent = self._agent_instances[agent_name]
|
||||
agent_invocations.labels(agent=agent_name).inc()
|
||||
|
||||
logger.info("Executing agent", agent=agent_name, state=self.state.to_dict())
|
||||
|
||||
try:
|
||||
# Run agent inference
|
||||
with StateGuard(self.state):
|
||||
result = await agent.run(self.state, self.history, self)
|
||||
|
||||
# Record in history
|
||||
self.history.add_inference(result)
|
||||
|
||||
# Execute any tool calls
|
||||
for tool_call in result.tool_calls:
|
||||
await self._execute_tool(agent, tool_call)
|
||||
|
||||
# Record state change
|
||||
self.state_history.record(self.state, f"after_{agent_name}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Agent execution failed", agent=agent_name, error=str(e))
|
||||
raise
|
||||
|
||||
async def _execute_tool(self, agent: Agent[S], tool_call: ToolCall):
|
||||
"""Execute a tool call."""
|
||||
logger.info("Executing tool", tool=tool_call.tool, args=tool_call.arguments)
|
||||
|
||||
try:
|
||||
# Execute tool
|
||||
result = await agent.execute_tool(tool_call.tool, tool_call.arguments, self)
|
||||
|
||||
# Record result
|
||||
self.history.add_tool_result(tool_call.tool, result, agent.name)
|
||||
|
||||
logger.info("Tool execution completed", tool=tool_call.tool, result=str(result)[:100])
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Tool execution failed", tool=tool_call.tool, error=str(e))
|
||||
self.history.add_tool_result(tool_call.tool, f"Error: {e}", agent.name)
|
||||
|
||||
async def _checkpoint(self, final: bool = False):
|
||||
"""Save checkpoint."""
|
||||
if not self.checkpoint_dir:
|
||||
return
|
||||
|
||||
self.checkpoint_dir.mkdir(exist_ok=True)
|
||||
|
||||
checkpoint_data = {
|
||||
"state": self.state.checkpoint(),
|
||||
"history": [entry.to_dict() for entry in self.history],
|
||||
"call_count": self.call_count,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"final": final
|
||||
}
|
||||
|
||||
filename = f"checkpoint_{self.call_count:04d}.json"
|
||||
if final:
|
||||
filename = "checkpoint_final.json"
|
||||
|
||||
path = self.checkpoint_dir / filename
|
||||
|
||||
with open(path, 'w') as f:
|
||||
json.dump(checkpoint_data, f, indent=2)
|
||||
|
||||
logger.info("Checkpoint saved", path=str(path))
|
||||
|
||||
@classmethod
|
||||
async def restore(cls, checkpoint_path: Path, **kwargs) -> "Network[S]":
|
||||
"""Restore network from checkpoint."""
|
||||
with open(checkpoint_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Restore state
|
||||
state_class = kwargs.get("state_class", State)
|
||||
state = state_class.restore(data["state"])
|
||||
|
||||
# Create network
|
||||
network = cls(state=state, **kwargs)
|
||||
|
||||
# Restore history
|
||||
for entry_data in data["history"]:
|
||||
network.history.append(History.from_dict(entry_data))
|
||||
|
||||
network.call_count = data["call_count"]
|
||||
|
||||
logger.info("Network restored from checkpoint", path=str(checkpoint_path))
|
||||
|
||||
return network
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Router system for agent orchestration."""
|
||||
|
||||
from typing import TypeVar, Callable, Optional, List, Type, Dict, Any
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
|
||||
from hanzo_agents.core.agent import Agent, InferenceResult
|
||||
from hanzo_agents.core.state import State
|
||||
|
||||
|
||||
S = TypeVar("S", bound=State)
|
||||
|
||||
# Router function type
|
||||
RouterFn = Callable[
|
||||
["Network[S]", int, Optional[InferenceResult], List[Type[Agent[S]]]],
|
||||
Optional[Type[Agent[S]]]
|
||||
]
|
||||
|
||||
|
||||
class RouterType(Enum):
|
||||
"""Types of routers."""
|
||||
DETERMINISTIC = "deterministic"
|
||||
LLM_BASED = "llm_based"
|
||||
HYBRID = "hybrid"
|
||||
|
||||
|
||||
class Router(ABC):
|
||||
"""Base router class."""
|
||||
|
||||
router_type: RouterType = RouterType.DETERMINISTIC
|
||||
|
||||
@abstractmethod
|
||||
def route(self,
|
||||
network: "Network[S]",
|
||||
call_count: int,
|
||||
last_result: Optional[InferenceResult],
|
||||
agent_stack: List[Type[Agent[S]]]) -> Optional[Type[Agent[S]]]:
|
||||
"""Determine next agent to run.
|
||||
|
||||
Args:
|
||||
network: Current network instance
|
||||
call_count: Number of agents called so far
|
||||
last_result: Result from previous agent
|
||||
agent_stack: Available agents
|
||||
|
||||
Returns:
|
||||
Next agent class or None to stop
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DeterministicRouter(Router):
|
||||
"""Code-based deterministic router."""
|
||||
|
||||
router_type = RouterType.DETERMINISTIC
|
||||
|
||||
def __init__(self, routing_fn: RouterFn[S]):
|
||||
"""Initialize with routing function."""
|
||||
self.routing_fn = routing_fn
|
||||
|
||||
def route(self, network, call_count, last_result, agent_stack):
|
||||
"""Use provided function for routing."""
|
||||
return self.routing_fn(network, call_count, last_result, agent_stack)
|
||||
|
||||
|
||||
class LLMRouter(Router):
|
||||
"""LLM-based router for dynamic decisions."""
|
||||
|
||||
router_type = RouterType.LLM_BASED
|
||||
|
||||
def __init__(self, model: str = "model://anthropic/claude-3-haiku"):
|
||||
"""Initialize with model."""
|
||||
self.model = model
|
||||
|
||||
async def route_async(self, network, call_count, last_result, agent_stack):
|
||||
"""Async routing with LLM call."""
|
||||
# Build context
|
||||
context = {
|
||||
"state": network.state.to_dict(),
|
||||
"call_count": call_count,
|
||||
"last_agent": last_result.agent if last_result else None,
|
||||
"available_agents": [
|
||||
{"name": agent.name, "description": agent.description}
|
||||
for agent in agent_stack
|
||||
]
|
||||
}
|
||||
|
||||
# Call LLM (mock for now)
|
||||
# In real implementation, this would use the model adapter
|
||||
prompt = f"""Given the current state and available agents, which agent should run next?
|
||||
|
||||
State: {context['state']}
|
||||
Last agent: {context['last_agent']}
|
||||
Available agents: {context['available_agents']}
|
||||
|
||||
Respond with just the agent name or 'none' to stop."""
|
||||
|
||||
# Mock response
|
||||
next_agent_name = "none" # Would come from LLM
|
||||
|
||||
if next_agent_name == "none":
|
||||
return None
|
||||
|
||||
# Find agent by name
|
||||
for agent in agent_stack:
|
||||
if agent.name == next_agent_name:
|
||||
return agent
|
||||
|
||||
return None
|
||||
|
||||
def route(self, network, call_count, last_result, agent_stack):
|
||||
"""Sync wrapper for compatibility."""
|
||||
# In practice, this would be handled by the network's async context
|
||||
import asyncio
|
||||
return asyncio.create_task(
|
||||
self.route_async(network, call_count, last_result, agent_stack)
|
||||
)
|
||||
|
||||
|
||||
class HybridRouter(Router):
|
||||
"""Combines deterministic and LLM routing."""
|
||||
|
||||
router_type = RouterType.HYBRID
|
||||
|
||||
def __init__(self,
|
||||
deterministic_fn: Optional[RouterFn[S]] = None,
|
||||
llm_model: str = "model://anthropic/claude-3-haiku",
|
||||
llm_threshold: float = 0.7):
|
||||
"""Initialize hybrid router.
|
||||
|
||||
Args:
|
||||
deterministic_fn: Primary routing function
|
||||
llm_model: Model for fallback routing
|
||||
llm_threshold: Confidence threshold for LLM routing
|
||||
"""
|
||||
self.deterministic_fn = deterministic_fn
|
||||
self.llm_router = LLMRouter(llm_model)
|
||||
self.llm_threshold = llm_threshold
|
||||
|
||||
def route(self, network, call_count, last_result, agent_stack):
|
||||
"""Try deterministic first, fall back to LLM."""
|
||||
# Try deterministic routing
|
||||
if self.deterministic_fn:
|
||||
next_agent = self.deterministic_fn(network, call_count, last_result, agent_stack)
|
||||
if next_agent is not None:
|
||||
return next_agent
|
||||
|
||||
# Fall back to LLM
|
||||
return self.llm_router.route(network, call_count, last_result, agent_stack)
|
||||
|
||||
|
||||
# Common routing patterns
|
||||
|
||||
def sequential_router(agent_sequence: List[Type[Agent[S]]]) -> RouterFn[S]:
|
||||
"""Create router that runs agents in sequence."""
|
||||
def router(network, call_count, last_result, agent_stack):
|
||||
if call_count < len(agent_sequence):
|
||||
return agent_sequence[call_count]
|
||||
return None
|
||||
return router
|
||||
|
||||
|
||||
def conditional_router(conditions: Dict[str, Type[Agent[S]]]) -> RouterFn[S]:
|
||||
"""Create router based on state conditions.
|
||||
|
||||
Args:
|
||||
conditions: Dict mapping condition names to agents
|
||||
|
||||
Example:
|
||||
router = conditional_router({
|
||||
"needs_plan": PlanningAgent,
|
||||
"needs_review": ReviewAgent,
|
||||
"needs_test": TestingAgent
|
||||
})
|
||||
"""
|
||||
def router(network, call_count, last_result, agent_stack):
|
||||
state_dict = network.state.to_dict()
|
||||
|
||||
# Check each condition
|
||||
for condition, agent in conditions.items():
|
||||
if state_dict.get(condition, False):
|
||||
return agent
|
||||
|
||||
return None
|
||||
return router
|
||||
|
||||
|
||||
def round_robin_router(agents: List[Type[Agent[S]]], max_rounds: int = 10) -> RouterFn[S]:
|
||||
"""Create router that cycles through agents."""
|
||||
def router(network, call_count, last_result, agent_stack):
|
||||
if call_count >= max_rounds * len(agents):
|
||||
return None
|
||||
return agents[call_count % len(agents)]
|
||||
return router
|
||||
|
||||
|
||||
def state_based_router(state_field: str,
|
||||
transitions: Dict[Any, Type[Agent[S]]],
|
||||
default: Optional[Type[Agent[S]]] = None) -> RouterFn[S]:
|
||||
"""Create router based on state field value.
|
||||
|
||||
Args:
|
||||
state_field: Name of state field to check
|
||||
transitions: Mapping of field values to agents
|
||||
default: Default agent if no match
|
||||
"""
|
||||
def router(network, call_count, last_result, agent_stack):
|
||||
state_dict = network.state.to_dict()
|
||||
value = state_dict.get(state_field)
|
||||
|
||||
if value in transitions:
|
||||
return transitions[value]
|
||||
|
||||
return default
|
||||
return router
|
||||
@@ -0,0 +1,145 @@
|
||||
"""State management for agent networks."""
|
||||
|
||||
from typing import TypeVar, Generic, Any, Dict, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class State:
|
||||
"""Base class for network state.
|
||||
|
||||
State should be:
|
||||
- Strongly typed (dataclass or Pydantic)
|
||||
- JSON serializable
|
||||
- Validated on every write
|
||||
|
||||
Example:
|
||||
@dataclass
|
||||
class ProjectState(State):
|
||||
repo: str
|
||||
plan: Optional[Plan] = None
|
||||
tests_passed: bool = False
|
||||
done: bool = False
|
||||
"""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert state to dictionary."""
|
||||
if isinstance(self, BaseModel):
|
||||
return self.model_dump()
|
||||
elif hasattr(self, "__dataclass_fields__"):
|
||||
return {
|
||||
k: v for k, v in self.__dict__.items()
|
||||
if not k.startswith("_")
|
||||
}
|
||||
else:
|
||||
return self.__dict__.copy()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "State":
|
||||
"""Create state from dictionary."""
|
||||
if issubclass(cls, BaseModel):
|
||||
return cls.model_validate(data)
|
||||
else:
|
||||
return cls(**data)
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate state integrity."""
|
||||
if isinstance(self, BaseModel):
|
||||
# Pydantic models self-validate
|
||||
pass
|
||||
else:
|
||||
# Override in subclasses for custom validation
|
||||
pass
|
||||
|
||||
def checkpoint(self) -> Dict[str, Any]:
|
||||
"""Create checkpoint data."""
|
||||
return {
|
||||
"state": self.to_dict(),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"type": self.__class__.__name__
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def restore(cls, checkpoint: Dict[str, Any]) -> "State":
|
||||
"""Restore from checkpoint."""
|
||||
return cls.from_dict(checkpoint["state"])
|
||||
|
||||
|
||||
# Type variable for generic state
|
||||
S = TypeVar("S", bound=State)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseState(State):
|
||||
"""Common base state with standard fields."""
|
||||
done: bool = False
|
||||
error: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class StateGuard:
|
||||
"""Guards state mutations for safety."""
|
||||
|
||||
def __init__(self, state: State):
|
||||
self._state = state
|
||||
self._original = state.to_dict()
|
||||
|
||||
def __enter__(self):
|
||||
return self._state
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is not None:
|
||||
# Rollback on exception
|
||||
for k, v in self._original.items():
|
||||
setattr(self._state, k, v)
|
||||
else:
|
||||
# Validate on success
|
||||
self._state.validate()
|
||||
|
||||
|
||||
class StateHistory:
|
||||
"""Track state changes over time."""
|
||||
|
||||
def __init__(self):
|
||||
self._history: list[Dict[str, Any]] = []
|
||||
|
||||
def record(self, state: State, event: str = "update"):
|
||||
"""Record state snapshot."""
|
||||
self._history.append({
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"event": event,
|
||||
"state": state.to_dict()
|
||||
})
|
||||
|
||||
def get_history(self) -> list[Dict[str, Any]]:
|
||||
"""Get full history."""
|
||||
return self._history.copy()
|
||||
|
||||
def diff(self, index1: int, index2: int) -> Dict[str, Any]:
|
||||
"""Compare two state snapshots."""
|
||||
if index1 >= len(self._history) or index2 >= len(self._history):
|
||||
raise IndexError("Invalid history indices")
|
||||
|
||||
state1 = self._history[index1]["state"]
|
||||
state2 = self._history[index2]["state"]
|
||||
|
||||
diff = {
|
||||
"added": {},
|
||||
"removed": {},
|
||||
"changed": {}
|
||||
}
|
||||
|
||||
# Find changes
|
||||
for k, v in state2.items():
|
||||
if k not in state1:
|
||||
diff["added"][k] = v
|
||||
elif state1[k] != v:
|
||||
diff["changed"][k] = {"from": state1[k], "to": v}
|
||||
|
||||
for k in state1:
|
||||
if k not in state2:
|
||||
diff["removed"][k] = state1[k]
|
||||
|
||||
return diff
|
||||
@@ -0,0 +1,411 @@
|
||||
"""Trusted Execution Environment (TEE) support for confidential agent computing.
|
||||
|
||||
This module provides interfaces for running agents in secure enclaves,
|
||||
enabling confidential AI computations with attestation capabilities.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Optional, Callable, List
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TEEProvider(Enum):
|
||||
"""Supported TEE providers."""
|
||||
INTEL_SGX = "sgx"
|
||||
AMD_SEV = "sev"
|
||||
NVIDIA_H100 = "h100"
|
||||
MOCK = "mock"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttestationReport:
|
||||
"""TEE attestation report."""
|
||||
|
||||
provider: TEEProvider
|
||||
enclave_id: str
|
||||
code_hash: str
|
||||
timestamp: float
|
||||
quote: bytes
|
||||
signature: str
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"provider": self.provider.value,
|
||||
"enclave_id": self.enclave_id,
|
||||
"code_hash": self.code_hash,
|
||||
"timestamp": self.timestamp,
|
||||
"quote": self.quote.hex() if isinstance(self.quote, bytes) else self.quote,
|
||||
"signature": self.signature,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
def verify(self, expected_code_hash: Optional[str] = None) -> bool:
|
||||
"""Verify attestation report.
|
||||
|
||||
In a real implementation, this would verify the TEE provider's
|
||||
signature on the quote and optionally check code hash.
|
||||
"""
|
||||
if expected_code_hash and self.code_hash != expected_code_hash:
|
||||
return False
|
||||
|
||||
# TODO: Implement actual verification based on provider
|
||||
# For now, just check signature format
|
||||
return len(self.signature) > 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TEEConfig:
|
||||
"""Configuration for TEE execution."""
|
||||
|
||||
provider: TEEProvider = TEEProvider.MOCK
|
||||
max_memory_mb: int = 4096
|
||||
max_execution_time_s: int = 300
|
||||
enable_network: bool = False
|
||||
allowed_endpoints: List[str] = field(default_factory=list)
|
||||
attestation_server: Optional[str] = None
|
||||
|
||||
|
||||
class TEEExecutor(ABC):
|
||||
"""Abstract interface for TEE execution."""
|
||||
|
||||
@abstractmethod
|
||||
def execute(
|
||||
self,
|
||||
code: str,
|
||||
inputs: Dict[str, Any],
|
||||
config: TEEConfig
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute code in TEE with given inputs."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_attestation(self) -> AttestationReport:
|
||||
"""Get attestation report for current execution."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify_remote_attestation(
|
||||
self,
|
||||
report: AttestationReport,
|
||||
expected_code_hash: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Verify a remote attestation report."""
|
||||
pass
|
||||
|
||||
|
||||
class MockTEEExecutor(TEEExecutor):
|
||||
"""Mock TEE executor for testing."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize mock TEE."""
|
||||
self.enclave_id = "mock_enclave_" + str(int(time.time()))
|
||||
self.last_code_hash = None
|
||||
self.last_result = None
|
||||
|
||||
def execute(
|
||||
self,
|
||||
code: str,
|
||||
inputs: Dict[str, Any],
|
||||
config: TEEConfig
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute code in mock TEE."""
|
||||
# Compute code hash
|
||||
self.last_code_hash = hashlib.sha256(code.encode()).hexdigest()
|
||||
|
||||
# Simulate execution
|
||||
# In real TEE, this would run in isolated enclave
|
||||
namespace = {"inputs": inputs, "result": None}
|
||||
|
||||
try:
|
||||
exec(code, namespace)
|
||||
self.last_result = namespace.get("result", {})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": self.last_result,
|
||||
"attestation": self.get_attestation().to_dict()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"attestation": self.get_attestation().to_dict()
|
||||
}
|
||||
|
||||
def get_attestation(self) -> AttestationReport:
|
||||
"""Get attestation for last execution."""
|
||||
return AttestationReport(
|
||||
provider=TEEProvider.MOCK,
|
||||
enclave_id=self.enclave_id,
|
||||
code_hash=self.last_code_hash or "",
|
||||
timestamp=time.time(),
|
||||
quote=b"mock_quote_data",
|
||||
signature="mock_signature_" + (self.last_code_hash or "")[:16],
|
||||
metadata={
|
||||
"mock": True,
|
||||
"result_hash": hashlib.sha256(
|
||||
json.dumps(self.last_result, sort_keys=True).encode()
|
||||
).hexdigest() if self.last_result else None
|
||||
}
|
||||
)
|
||||
|
||||
def verify_remote_attestation(
|
||||
self,
|
||||
report: AttestationReport,
|
||||
expected_code_hash: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Verify mock attestation."""
|
||||
return report.verify(expected_code_hash)
|
||||
|
||||
|
||||
class ConfidentialAgent:
|
||||
"""Agent that can execute in TEE for confidential computing."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent,
|
||||
tee_config: Optional[TEEConfig] = None,
|
||||
tee_executor: Optional[TEEExecutor] = None
|
||||
):
|
||||
"""Initialize confidential agent.
|
||||
|
||||
Args:
|
||||
agent: Base agent to wrap
|
||||
tee_config: TEE configuration
|
||||
tee_executor: TEE executor implementation
|
||||
"""
|
||||
self.agent = agent
|
||||
self.tee_config = tee_config or TEEConfig()
|
||||
self.tee_executor = tee_executor or MockTEEExecutor()
|
||||
self.attestation_history: List[AttestationReport] = []
|
||||
|
||||
def execute_confidential(
|
||||
self,
|
||||
task_code: str,
|
||||
inputs: Dict[str, Any],
|
||||
verify_code_hash: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute task in TEE.
|
||||
|
||||
Args:
|
||||
task_code: Python code to execute
|
||||
inputs: Input data for the task
|
||||
verify_code_hash: Expected code hash for verification
|
||||
|
||||
Returns:
|
||||
Execution result with attestation
|
||||
"""
|
||||
# Execute in TEE
|
||||
result = self.tee_executor.execute(
|
||||
task_code,
|
||||
inputs,
|
||||
self.tee_config
|
||||
)
|
||||
|
||||
# Get and store attestation
|
||||
if result.get("success"):
|
||||
attestation = AttestationReport(**result["attestation"])
|
||||
self.attestation_history.append(attestation)
|
||||
|
||||
# Verify if requested
|
||||
if verify_code_hash:
|
||||
if not attestation.verify(verify_code_hash):
|
||||
result["warning"] = "Code hash mismatch"
|
||||
|
||||
return result
|
||||
|
||||
def verify_computation(
|
||||
self,
|
||||
result: Dict[str, Any],
|
||||
expected_code_hash: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Verify a computation result came from valid TEE.
|
||||
|
||||
Args:
|
||||
result: Result dict with attestation
|
||||
expected_code_hash: Expected code hash
|
||||
|
||||
Returns:
|
||||
True if verification passes
|
||||
"""
|
||||
if "attestation" not in result:
|
||||
return False
|
||||
|
||||
attestation = AttestationReport(**result["attestation"])
|
||||
return self.tee_executor.verify_remote_attestation(
|
||||
attestation,
|
||||
expected_code_hash
|
||||
)
|
||||
|
||||
def get_attestation_history(self) -> List[AttestationReport]:
|
||||
"""Get history of attestations."""
|
||||
return self.attestation_history.copy()
|
||||
|
||||
|
||||
# Precompiled functions for on-chain TEE verification
|
||||
def create_attestation_verifier_tool():
|
||||
"""Create a tool for verifying TEE attestations."""
|
||||
from ..tool import Tool
|
||||
|
||||
class AttestationVerifierTool(Tool):
|
||||
"""Tool for verifying TEE attestations."""
|
||||
|
||||
def __init__(self):
|
||||
self.name = "verify_attestation"
|
||||
self.description = "Verify TEE attestation reports"
|
||||
|
||||
async def verify_attestation(
|
||||
self,
|
||||
attestation_dict: Dict[str, Any],
|
||||
expected_code_hash: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Verify a TEE attestation.
|
||||
|
||||
Args:
|
||||
attestation_dict: Attestation report as dict
|
||||
expected_code_hash: Expected code hash
|
||||
|
||||
Returns:
|
||||
Verification result
|
||||
"""
|
||||
try:
|
||||
report = AttestationReport(**attestation_dict)
|
||||
is_valid = report.verify(expected_code_hash)
|
||||
|
||||
return {
|
||||
"valid": is_valid,
|
||||
"provider": report.provider.value,
|
||||
"enclave_id": report.enclave_id,
|
||||
"code_hash": report.code_hash,
|
||||
"timestamp": report.timestamp
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def compare_attestations(
|
||||
self,
|
||||
attestation1: Dict[str, Any],
|
||||
attestation2: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Compare two attestation reports.
|
||||
|
||||
Args:
|
||||
attestation1: First attestation
|
||||
attestation2: Second attestation
|
||||
|
||||
Returns:
|
||||
Comparison result
|
||||
"""
|
||||
try:
|
||||
report1 = AttestationReport(**attestation1)
|
||||
report2 = AttestationReport(**attestation2)
|
||||
|
||||
return {
|
||||
"same_provider": report1.provider == report2.provider,
|
||||
"same_enclave": report1.enclave_id == report2.enclave_id,
|
||||
"same_code": report1.code_hash == report2.code_hash,
|
||||
"time_diff": abs(report1.timestamp - report2.timestamp)
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
return AttestationVerifierTool
|
||||
|
||||
|
||||
# Computation marketplace for TEE resources
|
||||
@dataclass
|
||||
class ComputeOffer:
|
||||
"""Offer to provide computation resources."""
|
||||
|
||||
provider_address: str
|
||||
provider_enclave_id: str
|
||||
price_per_second: float # In ETH
|
||||
max_duration: int # Seconds
|
||||
supported_providers: List[TEEProvider]
|
||||
attestation: Optional[AttestationReport] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComputeRequest:
|
||||
"""Request for computation resources."""
|
||||
|
||||
requester_address: str
|
||||
code_hash: str
|
||||
max_price_per_second: float
|
||||
required_duration: int
|
||||
required_provider: Optional[TEEProvider] = None
|
||||
|
||||
|
||||
class ComputeMarketplace:
|
||||
"""Marketplace for TEE compute resources."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize marketplace."""
|
||||
self.offers: Dict[str, ComputeOffer] = {}
|
||||
self.requests: Dict[str, ComputeRequest] = {}
|
||||
self.matches: List[Dict[str, Any]] = []
|
||||
|
||||
def post_offer(self, offer: ComputeOffer) -> str:
|
||||
"""Post a compute offer."""
|
||||
offer_id = f"offer_{len(self.offers)}"
|
||||
self.offers[offer_id] = offer
|
||||
self._try_match_offers()
|
||||
return offer_id
|
||||
|
||||
def post_request(self, request: ComputeRequest) -> str:
|
||||
"""Post a compute request."""
|
||||
request_id = f"request_{len(self.requests)}"
|
||||
self.requests[request_id] = request
|
||||
self._try_match_offers()
|
||||
return request_id
|
||||
|
||||
def _try_match_offers(self):
|
||||
"""Try to match offers with requests."""
|
||||
for req_id, request in list(self.requests.items()):
|
||||
for offer_id, offer in list(self.offers.items()):
|
||||
# Check if offer matches request
|
||||
if (offer.price_per_second <= request.max_price_per_second and
|
||||
offer.max_duration >= request.required_duration and
|
||||
(not request.required_provider or
|
||||
request.required_provider in offer.supported_providers)):
|
||||
|
||||
# Create match
|
||||
match = {
|
||||
"request_id": req_id,
|
||||
"offer_id": offer_id,
|
||||
"provider": offer.provider_address,
|
||||
"requester": request.requester_address,
|
||||
"price_per_second": offer.price_per_second,
|
||||
"duration": request.required_duration,
|
||||
"total_price": offer.price_per_second * request.required_duration,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
self.matches.append(match)
|
||||
|
||||
# Remove matched items
|
||||
del self.requests[req_id]
|
||||
del self.offers[offer_id]
|
||||
|
||||
break
|
||||
|
||||
def get_matches(self) -> List[Dict[str, Any]]:
|
||||
"""Get all matches."""
|
||||
return self.matches.copy()
|
||||
|
||||
def get_active_offers(self) -> Dict[str, ComputeOffer]:
|
||||
"""Get active offers."""
|
||||
return self.offers.copy()
|
||||
|
||||
def get_active_requests(self) -> Dict[str, ComputeRequest]:
|
||||
"""Get active requests."""
|
||||
return self.requests.copy()
|
||||
@@ -0,0 +1,234 @@
|
||||
"""Tool system for agent side-effects."""
|
||||
|
||||
from typing import TypeVar, Generic, Dict, Any, Optional, Type, Callable
|
||||
from abc import ABC, abstractmethod
|
||||
from pydantic import BaseModel
|
||||
import inspect
|
||||
from functools import wraps
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
|
||||
S = TypeVar("S") # State type
|
||||
|
||||
|
||||
class Tool(ABC, Generic[S]):
|
||||
"""Base class for agent tools.
|
||||
|
||||
Tools are the ONLY way agents can perform side-effects.
|
||||
All tool operations must be:
|
||||
- Deterministic
|
||||
- Typed (via Parameters model)
|
||||
- State-mutating in an explicit way
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
parameters: Type[BaseModel] = BaseModel
|
||||
|
||||
@abstractmethod
|
||||
def handle(self, **kwargs) -> Any:
|
||||
"""Execute the tool operation.
|
||||
|
||||
The network parameter is injected automatically and provides
|
||||
access to state and other network resources.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_schema(self) -> Dict[str, Any]:
|
||||
"""Get JSON schema for tool parameters."""
|
||||
if hasattr(self.parameters, "model_json_schema"):
|
||||
schema = self.parameters.model_json_schema()
|
||||
else:
|
||||
schema = {"type": "object", "properties": {}}
|
||||
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": schema
|
||||
}
|
||||
|
||||
def validate_params(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Validate and parse parameters."""
|
||||
if self.parameters and self.parameters != BaseModel:
|
||||
# Use Pydantic for validation
|
||||
params = self.parameters(**kwargs)
|
||||
return params.model_dump()
|
||||
return kwargs
|
||||
|
||||
async def __call__(self, network: "Network[S]", **kwargs) -> Any:
|
||||
"""Execute tool with network context."""
|
||||
# Validate parameters
|
||||
validated = self.validate_params(**kwargs)
|
||||
|
||||
# Inject network into handle method
|
||||
sig = inspect.signature(self.handle)
|
||||
if "network" in sig.parameters:
|
||||
validated["network"] = network
|
||||
|
||||
# Execute
|
||||
if asyncio.iscoroutinefunction(self.handle):
|
||||
result = await self.handle(**validated)
|
||||
else:
|
||||
result = self.handle(**validated)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Global registry for available tools."""
|
||||
|
||||
_tools: Dict[str, Type[Tool]] = {}
|
||||
_instances: Dict[str, Tool] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, tool_class: Type[Tool], name: Optional[str] = None):
|
||||
"""Register a tool class."""
|
||||
name = name or tool_class.name
|
||||
cls._tools[name] = tool_class
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> Optional[Type[Tool]]:
|
||||
"""Get tool class by name."""
|
||||
return cls._tools.get(name)
|
||||
|
||||
@classmethod
|
||||
def list_tools(cls) -> list[str]:
|
||||
"""List all registered tool names."""
|
||||
return list(cls._tools.keys())
|
||||
|
||||
@classmethod
|
||||
def create_instance(cls, name: str, **kwargs) -> Tool:
|
||||
"""Create tool instance."""
|
||||
if name not in cls._instances:
|
||||
tool_class = cls.get(name)
|
||||
if not tool_class:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
cls._instances[name] = tool_class(**kwargs)
|
||||
return cls._instances[name]
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
"""Clear all registrations (for testing)."""
|
||||
cls._tools.clear()
|
||||
cls._instances.clear()
|
||||
|
||||
|
||||
def tool(name: str, description: str, parameters: Optional[Type[BaseModel]] = None):
|
||||
"""Decorator to create tools from functions."""
|
||||
|
||||
def decorator(func: Callable) -> Type[Tool]:
|
||||
# Extract parameter schema from function signature if not provided
|
||||
if parameters is None:
|
||||
sig = inspect.signature(func)
|
||||
fields = {}
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name in ["self", "network"]:
|
||||
continue
|
||||
# Simple type mapping
|
||||
param_type = param.annotation if param.annotation != inspect.Parameter.empty else Any
|
||||
fields[param_name] = (param_type, param.default if param.default != inspect.Parameter.empty else ...)
|
||||
|
||||
# Create Pydantic model dynamically
|
||||
Parameters = type("Parameters", (BaseModel,), {"__annotations__": fields})
|
||||
else:
|
||||
Parameters = parameters
|
||||
|
||||
# Create tool class
|
||||
class FunctionTool(Tool):
|
||||
def __init__(self):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.parameters = Parameters
|
||||
|
||||
def handle(self, **kwargs):
|
||||
return func(**kwargs)
|
||||
|
||||
# Register and return
|
||||
ToolRegistry.register(FunctionTool, name)
|
||||
return FunctionTool
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# Built-in tools
|
||||
|
||||
class ThinkTool(Tool[S]):
|
||||
"""Tool for agents to think/reason without side effects."""
|
||||
|
||||
name = "think"
|
||||
description = "Think about the problem and plan next steps"
|
||||
|
||||
class Parameters(BaseModel):
|
||||
thought: str
|
||||
|
||||
def handle(self, thought: str, network: "Network[S]") -> str:
|
||||
"""Record thought in history."""
|
||||
# No state mutation, just returns for history
|
||||
return f"Thought: {thought}"
|
||||
|
||||
|
||||
class MemoryStoreTool(Tool[S]):
|
||||
"""Store information in long-term memory."""
|
||||
|
||||
name = "memory_store"
|
||||
description = "Store information for future reference"
|
||||
|
||||
class Parameters(BaseModel):
|
||||
key: str
|
||||
value: Dict[str, Any]
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
async def handle(self, key: str, value: Dict[str, Any],
|
||||
metadata: Optional[Dict[str, Any]],
|
||||
network: "Network[S]") -> str:
|
||||
"""Store in memory."""
|
||||
if network.memory_kv:
|
||||
await network.memory_kv.save(key, {
|
||||
"value": value,
|
||||
"metadata": metadata or {}
|
||||
})
|
||||
return f"Stored '{key}' in memory"
|
||||
return "No memory backend available"
|
||||
|
||||
|
||||
class MemoryRecallTool(Tool[S]):
|
||||
"""Recall information from memory."""
|
||||
|
||||
name = "memory_recall"
|
||||
description = "Recall stored information"
|
||||
|
||||
class Parameters(BaseModel):
|
||||
key: str
|
||||
|
||||
async def handle(self, key: str, network: "Network[S]") -> Any:
|
||||
"""Recall from memory."""
|
||||
if network.memory_kv:
|
||||
data = await network.memory_kv.load(key)
|
||||
if data:
|
||||
return data.get("value", data)
|
||||
return None
|
||||
|
||||
|
||||
class MemorySearchTool(Tool[S]):
|
||||
"""Search vector memory."""
|
||||
|
||||
name = "memory_search"
|
||||
description = "Search for relevant memories"
|
||||
|
||||
class Parameters(BaseModel):
|
||||
query: str
|
||||
k: int = 5
|
||||
|
||||
async def handle(self, query: str, k: int, network: "Network[S]") -> list[str]:
|
||||
"""Search vector memory."""
|
||||
if network.memory_vector:
|
||||
return await network.memory_vector.query(query, k=k)
|
||||
return []
|
||||
|
||||
|
||||
# Register built-in tools
|
||||
ToolRegistry.register(ThinkTool)
|
||||
ToolRegistry.register(MemoryStoreTool)
|
||||
ToolRegistry.register(MemoryRecallTool)
|
||||
ToolRegistry.register(MemorySearchTool)
|
||||
@@ -0,0 +1,411 @@
|
||||
"""Wallet and Web3 integration for agents.
|
||||
|
||||
This module provides wallet capabilities for agents to interact with blockchain networks,
|
||||
enabling on-chain payments, identity, and decentralized coordination.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Union, List
|
||||
from eth_account import Account
|
||||
from eth_account.hdaccount import generate_mnemonic
|
||||
from eth_keys import keys
|
||||
import secrets
|
||||
|
||||
# Try to import web3 dependencies
|
||||
try:
|
||||
from web3 import Web3
|
||||
from web3.types import TxParams, Wei
|
||||
from eth_typing import Address, HexStr
|
||||
WEB3_AVAILABLE = True
|
||||
except ImportError:
|
||||
WEB3_AVAILABLE = False
|
||||
Web3 = None
|
||||
TxParams = Dict[str, Any]
|
||||
Wei = int
|
||||
Address = str
|
||||
HexStr = str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WalletConfig:
|
||||
"""Configuration for agent wallet."""
|
||||
|
||||
private_key: Optional[str] = None
|
||||
mnemonic: Optional[str] = None
|
||||
account_index: int = 0
|
||||
network_rpc: str = "http://localhost:8545"
|
||||
chain_id: int = 31337 # Default to local hardhat/anvil
|
||||
gas_limit: int = 3000000
|
||||
gas_price_gwei: int = 20
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate configuration."""
|
||||
if not self.private_key and not self.mnemonic:
|
||||
# Generate a new random private key if none provided
|
||||
self.private_key = "0x" + secrets.token_hex(32)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transaction:
|
||||
"""Represents a blockchain transaction."""
|
||||
|
||||
hash: str
|
||||
from_address: str
|
||||
to_address: str
|
||||
value: Wei
|
||||
gas_used: Optional[int] = None
|
||||
status: Optional[bool] = None
|
||||
block_number: Optional[int] = None
|
||||
|
||||
|
||||
class WalletInterface(ABC):
|
||||
"""Abstract interface for wallet implementations."""
|
||||
|
||||
@abstractmethod
|
||||
def get_address(self) -> str:
|
||||
"""Get the wallet's address."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_balance(self) -> Wei:
|
||||
"""Get the wallet's balance in Wei."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def sign_message(self, message: str) -> str:
|
||||
"""Sign a message with the wallet's private key."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def send_transaction(
|
||||
self,
|
||||
to: str,
|
||||
value: Wei,
|
||||
data: Optional[bytes] = None,
|
||||
gas_limit: Optional[int] = None,
|
||||
gas_price: Optional[Wei] = None
|
||||
) -> Transaction:
|
||||
"""Send a transaction."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def call_contract(
|
||||
self,
|
||||
contract_address: str,
|
||||
function_signature: str,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""Call a smart contract function."""
|
||||
pass
|
||||
|
||||
|
||||
class Web3Wallet(WalletInterface):
|
||||
"""Web3-based wallet implementation."""
|
||||
|
||||
def __init__(self, config: WalletConfig):
|
||||
"""Initialize Web3 wallet."""
|
||||
if not WEB3_AVAILABLE:
|
||||
raise ImportError(
|
||||
"Web3 dependencies not available. "
|
||||
"Install with: pip install web3 eth-account"
|
||||
)
|
||||
|
||||
self.config = config
|
||||
self.w3 = Web3(Web3.HTTPProvider(config.network_rpc))
|
||||
|
||||
# Initialize account from private key or mnemonic
|
||||
if config.private_key:
|
||||
self.account = Account.from_key(config.private_key)
|
||||
elif config.mnemonic:
|
||||
# Derive account from mnemonic at given index
|
||||
Account.enable_unaudited_hdwallet_features()
|
||||
self.account = Account.from_mnemonic(
|
||||
config.mnemonic,
|
||||
account_path=f"m/44'/60'/0'/0/{config.account_index}"
|
||||
)
|
||||
else:
|
||||
raise ValueError("Either private_key or mnemonic must be provided")
|
||||
|
||||
# Ensure connection
|
||||
if not self.w3.is_connected():
|
||||
raise ConnectionError(f"Cannot connect to {config.network_rpc}")
|
||||
|
||||
def get_address(self) -> str:
|
||||
"""Get the wallet's address."""
|
||||
return self.account.address
|
||||
|
||||
def get_balance(self) -> Wei:
|
||||
"""Get the wallet's balance in Wei."""
|
||||
return self.w3.eth.get_balance(self.account.address)
|
||||
|
||||
def sign_message(self, message: str) -> str:
|
||||
"""Sign a message with the wallet's private key."""
|
||||
message_hash = hashlib.sha256(message.encode()).digest()
|
||||
signed = self.account.signHash(message_hash)
|
||||
return signed.signature.hex()
|
||||
|
||||
def send_transaction(
|
||||
self,
|
||||
to: str,
|
||||
value: Wei,
|
||||
data: Optional[bytes] = None,
|
||||
gas_limit: Optional[int] = None,
|
||||
gas_price: Optional[Wei] = None
|
||||
) -> Transaction:
|
||||
"""Send a transaction."""
|
||||
# Build transaction
|
||||
tx: TxParams = {
|
||||
'from': self.account.address,
|
||||
'to': to,
|
||||
'value': value,
|
||||
'gas': gas_limit or self.config.gas_limit,
|
||||
'gasPrice': gas_price or Web3.to_wei(self.config.gas_price_gwei, 'gwei'),
|
||||
'nonce': self.w3.eth.get_transaction_count(self.account.address),
|
||||
'chainId': self.config.chain_id,
|
||||
}
|
||||
|
||||
if data:
|
||||
tx['data'] = data
|
||||
|
||||
# Sign transaction
|
||||
signed_tx = self.account.sign_transaction(tx)
|
||||
|
||||
# Send transaction
|
||||
tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
# Wait for receipt
|
||||
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
return Transaction(
|
||||
hash=tx_hash.hex(),
|
||||
from_address=self.account.address,
|
||||
to_address=to,
|
||||
value=value,
|
||||
gas_used=receipt['gasUsed'],
|
||||
status=receipt['status'] == 1,
|
||||
block_number=receipt['blockNumber']
|
||||
)
|
||||
|
||||
def call_contract(
|
||||
self,
|
||||
contract_address: str,
|
||||
function_signature: str,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""Call a smart contract function.
|
||||
|
||||
This is a simplified interface. For complex contracts,
|
||||
use web3.py directly with the contract ABI.
|
||||
"""
|
||||
# This would need the contract ABI for proper encoding
|
||||
# For now, raise NotImplementedError
|
||||
raise NotImplementedError(
|
||||
"Contract calls require ABI. Use web3.py directly for now."
|
||||
)
|
||||
|
||||
|
||||
class MockWallet(WalletInterface):
|
||||
"""Mock wallet for testing without blockchain."""
|
||||
|
||||
def __init__(self, config: WalletConfig):
|
||||
"""Initialize mock wallet."""
|
||||
self.config = config
|
||||
self.address = "0x" + hashlib.sha256(
|
||||
(config.private_key or config.mnemonic or "mock").encode()
|
||||
).hexdigest()[:40]
|
||||
self.balance = Wei(1000000000000000000000) # 1000 ETH
|
||||
self.transactions: List[Transaction] = []
|
||||
|
||||
def get_address(self) -> str:
|
||||
"""Get the wallet's address."""
|
||||
return self.address
|
||||
|
||||
def get_balance(self) -> Wei:
|
||||
"""Get the wallet's balance in Wei."""
|
||||
return self.balance
|
||||
|
||||
def sign_message(self, message: str) -> str:
|
||||
"""Sign a message (mock)."""
|
||||
return "0x" + hashlib.sha256(
|
||||
f"{self.address}:{message}".encode()
|
||||
).hexdigest()
|
||||
|
||||
def send_transaction(
|
||||
self,
|
||||
to: str,
|
||||
value: Wei,
|
||||
data: Optional[bytes] = None,
|
||||
gas_limit: Optional[int] = None,
|
||||
gas_price: Optional[Wei] = None
|
||||
) -> Transaction:
|
||||
"""Send a transaction (mock)."""
|
||||
if value > self.balance:
|
||||
raise ValueError("Insufficient balance")
|
||||
|
||||
self.balance -= value
|
||||
|
||||
tx = Transaction(
|
||||
hash="0x" + secrets.token_hex(32),
|
||||
from_address=self.address,
|
||||
to_address=to,
|
||||
value=value,
|
||||
gas_used=21000,
|
||||
status=True,
|
||||
block_number=len(self.transactions)
|
||||
)
|
||||
|
||||
self.transactions.append(tx)
|
||||
return tx
|
||||
|
||||
def call_contract(
|
||||
self,
|
||||
contract_address: str,
|
||||
function_signature: str,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""Call a smart contract function (mock)."""
|
||||
return f"Mock result for {function_signature}"
|
||||
|
||||
|
||||
class AgentWallet:
|
||||
"""High-level wallet interface for agents."""
|
||||
|
||||
def __init__(self, config: Optional[WalletConfig] = None):
|
||||
"""Initialize agent wallet."""
|
||||
self.config = config or WalletConfig()
|
||||
|
||||
# Use mock wallet if web3 not available or in test mode
|
||||
if WEB3_AVAILABLE and not self.config.network_rpc.startswith("mock://"):
|
||||
self.wallet = Web3Wallet(self.config)
|
||||
else:
|
||||
self.wallet = MockWallet(self.config)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
"""Get wallet address."""
|
||||
return self.wallet.get_address()
|
||||
|
||||
@property
|
||||
def balance(self) -> Wei:
|
||||
"""Get wallet balance."""
|
||||
return self.wallet.get_balance()
|
||||
|
||||
def send_payment(
|
||||
self,
|
||||
to: str,
|
||||
amount_ether: float,
|
||||
memo: Optional[str] = None
|
||||
) -> Transaction:
|
||||
"""Send a payment to another address.
|
||||
|
||||
Args:
|
||||
to: Recipient address
|
||||
amount_ether: Amount in Ether (not Wei)
|
||||
memo: Optional memo (stored off-chain)
|
||||
|
||||
Returns:
|
||||
Transaction object
|
||||
"""
|
||||
amount_wei = Wei(int(amount_ether * 10**18))
|
||||
|
||||
# Log memo if provided (would be stored in agent memory)
|
||||
if memo:
|
||||
print(f"Payment memo: {memo}")
|
||||
|
||||
return self.wallet.send_transaction(to, amount_wei)
|
||||
|
||||
def sign_message(self, message: str) -> str:
|
||||
"""Sign a message for authentication."""
|
||||
return self.wallet.sign_message(message)
|
||||
|
||||
def verify_signature(
|
||||
self,
|
||||
message: str,
|
||||
signature: str,
|
||||
expected_address: str
|
||||
) -> bool:
|
||||
"""Verify a signature matches expected address.
|
||||
|
||||
This is a simplified version. Real implementation would
|
||||
recover the address from signature and compare.
|
||||
"""
|
||||
# TODO: Implement proper signature verification
|
||||
return True # Placeholder
|
||||
|
||||
|
||||
def generate_shared_mnemonic() -> str:
|
||||
"""Generate a shared mnemonic for a network of agents."""
|
||||
return generate_mnemonic(num_words=12, lang="english")
|
||||
|
||||
|
||||
def derive_agent_wallet(mnemonic: str, agent_index: int, **kwargs) -> AgentWallet:
|
||||
"""Derive an agent wallet from shared mnemonic.
|
||||
|
||||
Args:
|
||||
mnemonic: Shared network mnemonic
|
||||
agent_index: Unique index for this agent
|
||||
**kwargs: Additional wallet config options
|
||||
|
||||
Returns:
|
||||
AgentWallet instance
|
||||
"""
|
||||
config = WalletConfig(
|
||||
mnemonic=mnemonic,
|
||||
account_index=agent_index,
|
||||
**kwargs
|
||||
)
|
||||
return AgentWallet(config)
|
||||
|
||||
|
||||
# Tool functions for agent use
|
||||
def create_wallet_tool():
|
||||
"""Create a tool that agents can use for wallet operations."""
|
||||
from ..tool import Tool
|
||||
|
||||
class WalletTool(Tool):
|
||||
"""Tool for wallet operations."""
|
||||
|
||||
def __init__(self, wallet: AgentWallet):
|
||||
self.wallet = wallet
|
||||
self.name = "wallet"
|
||||
self.description = "Interact with blockchain wallet"
|
||||
|
||||
async def get_balance(self) -> float:
|
||||
"""Get wallet balance in Ether."""
|
||||
balance_wei = self.wallet.balance
|
||||
return float(balance_wei) / 10**18
|
||||
|
||||
async def send_payment(
|
||||
self,
|
||||
to_address: str,
|
||||
amount_ether: float,
|
||||
reason: Optional[str] = None
|
||||
) -> str:
|
||||
"""Send payment to another address.
|
||||
|
||||
Args:
|
||||
to_address: Recipient blockchain address
|
||||
amount_ether: Amount to send in Ether
|
||||
reason: Optional reason for payment
|
||||
|
||||
Returns:
|
||||
Transaction hash
|
||||
"""
|
||||
tx = self.wallet.send_payment(to_address, amount_ether, reason)
|
||||
return f"Sent {amount_ether} ETH to {to_address}. Tx: {tx.hash}"
|
||||
|
||||
async def get_address(self) -> str:
|
||||
"""Get this wallet's address."""
|
||||
return self.wallet.address
|
||||
|
||||
async def sign_message(self, message: str) -> str:
|
||||
"""Sign a message for authentication."""
|
||||
return self.wallet.sign_message(message)
|
||||
|
||||
return WalletTool
|
||||
@@ -0,0 +1,382 @@
|
||||
"""Web3-enabled agent with wallet and TEE support."""
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .agent import Agent, InferenceResult, ToolCall
|
||||
from .state import State
|
||||
from .wallet import AgentWallet, WalletConfig, Transaction, create_wallet_tool
|
||||
from .tee import (
|
||||
ConfidentialAgent, TEEConfig, TEEProvider,
|
||||
AttestationReport, create_attestation_verifier_tool
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Web3AgentConfig:
|
||||
"""Configuration for Web3-enabled agents."""
|
||||
|
||||
# Wallet configuration
|
||||
wallet_enabled: bool = False
|
||||
wallet_config: Optional[WalletConfig] = None
|
||||
|
||||
# TEE configuration
|
||||
tee_enabled: bool = False
|
||||
tee_config: Optional[TEEConfig] = None
|
||||
tee_provider: TEEProvider = TEEProvider.MOCK
|
||||
|
||||
# Economic parameters
|
||||
min_payment_eth: float = 0.001 # Minimum payment to accept
|
||||
task_price_eth: float = 0.01 # Default price for tasks
|
||||
reputation_threshold: float = 0.8 # Min reputation to work with
|
||||
|
||||
# On-chain identity
|
||||
agent_nft_address: Optional[str] = None
|
||||
reputation_contract: Optional[str] = None
|
||||
|
||||
|
||||
class Web3Agent(Agent):
|
||||
"""Agent with Web3 capabilities including wallet and TEE support."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
description: str,
|
||||
web3_config: Optional[Web3AgentConfig] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""Initialize Web3-enabled agent.
|
||||
|
||||
Args:
|
||||
name: Agent name
|
||||
description: Agent description
|
||||
web3_config: Web3 configuration
|
||||
**kwargs: Additional agent parameters
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.web3_config = web3_config or Web3AgentConfig()
|
||||
|
||||
# Initialize wallet if enabled
|
||||
self.wallet: Optional[AgentWallet] = None
|
||||
if self.web3_config.wallet_enabled:
|
||||
wallet_config = self.web3_config.wallet_config or WalletConfig()
|
||||
self.wallet = AgentWallet(wallet_config)
|
||||
|
||||
# Add wallet tool to agent's tools
|
||||
WalletTool = create_wallet_tool()
|
||||
self.wallet_tool = WalletTool(self.wallet)
|
||||
if not hasattr(self, 'tools'):
|
||||
self.tools = []
|
||||
self.tools.append(self.wallet_tool)
|
||||
|
||||
# Initialize TEE wrapper if enabled
|
||||
self.confidential_agent: Optional[ConfidentialAgent] = None
|
||||
if self.web3_config.tee_enabled:
|
||||
self.confidential_agent = ConfidentialAgent(
|
||||
self,
|
||||
self.web3_config.tee_config
|
||||
)
|
||||
|
||||
# Add attestation verifier tool
|
||||
AttestationTool = create_attestation_verifier_tool()
|
||||
self.attestation_tool = AttestationTool()
|
||||
if not hasattr(self, 'tools'):
|
||||
self.tools = []
|
||||
self.tools.append(self.attestation_tool)
|
||||
|
||||
# Track economic activity
|
||||
self.earnings: float = 0.0
|
||||
self.spending: float = 0.0
|
||||
self.completed_tasks: int = 0
|
||||
self.reputation_score: float = 1.0
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[str]:
|
||||
"""Get agent's blockchain address."""
|
||||
return self.wallet.address if self.wallet else None
|
||||
|
||||
@property
|
||||
def balance_eth(self) -> float:
|
||||
"""Get wallet balance in ETH."""
|
||||
if not self.wallet:
|
||||
return 0.0
|
||||
return float(self.wallet.balance) / 10**18
|
||||
|
||||
async def request_payment(
|
||||
self,
|
||||
from_address: str,
|
||||
amount_eth: float,
|
||||
task_description: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Request payment for a task.
|
||||
|
||||
Args:
|
||||
from_address: Payer's address
|
||||
amount_eth: Payment amount in ETH
|
||||
task_description: Description of the task
|
||||
|
||||
Returns:
|
||||
Payment request details
|
||||
"""
|
||||
if not self.wallet:
|
||||
return {"error": "Wallet not enabled"}
|
||||
|
||||
if amount_eth < self.web3_config.min_payment_eth:
|
||||
return {
|
||||
"error": f"Payment too low. Minimum: {self.web3_config.min_payment_eth} ETH"
|
||||
}
|
||||
|
||||
return {
|
||||
"to": self.wallet.address,
|
||||
"amount_eth": amount_eth,
|
||||
"task": task_description,
|
||||
"request_id": f"req_{self.name}_{self.completed_tasks + 1}"
|
||||
}
|
||||
|
||||
async def verify_payment(
|
||||
self,
|
||||
tx_hash: str,
|
||||
expected_amount_eth: float
|
||||
) -> bool:
|
||||
"""Verify a payment was received.
|
||||
|
||||
In a real implementation, this would check the blockchain.
|
||||
For now, we'll use a simplified version.
|
||||
"""
|
||||
# TODO: Implement blockchain verification
|
||||
# For now, just track it
|
||||
self.earnings += expected_amount_eth
|
||||
return True
|
||||
|
||||
async def pay_agent(
|
||||
self,
|
||||
to_address: str,
|
||||
amount_eth: float,
|
||||
reason: str
|
||||
) -> Optional[Transaction]:
|
||||
"""Pay another agent.
|
||||
|
||||
Args:
|
||||
to_address: Recipient agent's address
|
||||
amount_eth: Payment amount
|
||||
reason: Reason for payment
|
||||
|
||||
Returns:
|
||||
Transaction object if successful
|
||||
"""
|
||||
if not self.wallet:
|
||||
print("Wallet not enabled")
|
||||
return None
|
||||
|
||||
if amount_eth > self.balance_eth:
|
||||
print(f"Insufficient balance. Have: {self.balance_eth}, Need: {amount_eth}")
|
||||
return None
|
||||
|
||||
try:
|
||||
tx = self.wallet.send_payment(to_address, amount_eth, reason)
|
||||
self.spending += amount_eth
|
||||
return tx
|
||||
except Exception as e:
|
||||
print(f"Payment failed: {e}")
|
||||
return None
|
||||
|
||||
async def execute_confidential(
|
||||
self,
|
||||
task_code: str,
|
||||
inputs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute task in TEE for confidentiality.
|
||||
|
||||
Args:
|
||||
task_code: Python code to execute
|
||||
inputs: Input data for the task
|
||||
|
||||
Returns:
|
||||
Execution result with attestation
|
||||
"""
|
||||
if not self.confidential_agent:
|
||||
return {"error": "TEE not enabled"}
|
||||
|
||||
result = self.confidential_agent.execute_confidential(
|
||||
task_code,
|
||||
inputs
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def run(self, state: State, history, network) -> InferenceResult:
|
||||
"""Execute agent with Web3 enhancements.
|
||||
|
||||
This adds economic and TEE considerations to agent execution.
|
||||
"""
|
||||
# Check if this is a paid task
|
||||
task_payment = state.get("task_payment", 0)
|
||||
if task_payment > 0:
|
||||
# Verify payment before proceeding
|
||||
tx_hash = state.get("payment_tx")
|
||||
if tx_hash:
|
||||
verified = await self.verify_payment(tx_hash, task_payment)
|
||||
if not verified:
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content="Payment verification failed. Cannot proceed with task.",
|
||||
metadata={"payment_required": True}
|
||||
)
|
||||
|
||||
# Check if confidential execution is requested
|
||||
if state.get("require_tee", False) and self.confidential_agent:
|
||||
# Execute in TEE
|
||||
task_code = state.get("task_code", "")
|
||||
task_inputs = state.get("task_inputs", {})
|
||||
|
||||
tee_result = await self.execute_confidential(
|
||||
task_code,
|
||||
task_inputs
|
||||
)
|
||||
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content="Task executed in TEE",
|
||||
metadata={
|
||||
"tee_result": tee_result,
|
||||
"attestation": tee_result.get("attestation")
|
||||
}
|
||||
)
|
||||
|
||||
# Regular execution - must be implemented by subclass
|
||||
return await self._run_impl(state, history, network)
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> InferenceResult:
|
||||
"""Actual agent implementation - override in subclass."""
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content=f"{self.name} is ready to work. Balance: {self.balance_eth:.4f} ETH"
|
||||
)
|
||||
|
||||
def update_reputation(self, delta: float):
|
||||
"""Update agent's reputation score.
|
||||
|
||||
Args:
|
||||
delta: Change in reputation (-1 to 1)
|
||||
"""
|
||||
self.reputation_score = max(0, min(1, self.reputation_score + delta))
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get agent statistics."""
|
||||
stats = {
|
||||
"name": self.name,
|
||||
"address": self.address,
|
||||
"balance_eth": self.balance_eth,
|
||||
"earnings_eth": self.earnings,
|
||||
"spending_eth": self.spending,
|
||||
"completed_tasks": self.completed_tasks,
|
||||
"reputation": self.reputation_score,
|
||||
"wallet_enabled": self.web3_config.wallet_enabled,
|
||||
"tee_enabled": self.web3_config.tee_enabled
|
||||
}
|
||||
|
||||
if self.confidential_agent:
|
||||
stats["attestations"] = len(
|
||||
self.confidential_agent.get_attestation_history()
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# Example Web3-enabled agents
|
||||
|
||||
class DataProviderAgent(Web3Agent):
|
||||
"""Agent that provides data for payment."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="data_provider",
|
||||
description="Provides high-quality data for AI training",
|
||||
**kwargs
|
||||
)
|
||||
self.data_catalog = {
|
||||
"weather": {"price_eth": 0.01, "size_mb": 100},
|
||||
"finance": {"price_eth": 0.05, "size_mb": 500},
|
||||
"research": {"price_eth": 0.1, "size_mb": 1000}
|
||||
}
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> InferenceResult:
|
||||
"""Provide data based on request."""
|
||||
request = state.get("data_request", {})
|
||||
dataset = request.get("dataset")
|
||||
|
||||
if dataset not in self.data_catalog:
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content=f"Unknown dataset: {dataset}. Available: {list(self.data_catalog.keys())}"
|
||||
)
|
||||
|
||||
data_info = self.data_catalog[dataset]
|
||||
|
||||
# Request payment
|
||||
payment_request = await self.request_payment(
|
||||
from_address=request.get("requester_address", ""),
|
||||
amount_eth=data_info["price_eth"],
|
||||
task_description=f"Provide {dataset} dataset"
|
||||
)
|
||||
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content=f"Dataset {dataset} available for {data_info['price_eth']} ETH",
|
||||
metadata={
|
||||
"payment_request": payment_request,
|
||||
"data_info": data_info
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ComputeProviderAgent(Web3Agent):
|
||||
"""Agent that provides GPU compute for payment."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="compute_provider",
|
||||
description="Provides GPU compute resources",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
task_price_eth=0.1 # Per hour
|
||||
),
|
||||
**kwargs
|
||||
)
|
||||
self.gpu_specs = {
|
||||
"model": "NVIDIA H100",
|
||||
"memory_gb": 80,
|
||||
"tflops": 1000
|
||||
}
|
||||
|
||||
async def _run_impl(self, state: State, history, network) -> InferenceResult:
|
||||
"""Offer compute resources."""
|
||||
compute_request = state.get("compute_request", {})
|
||||
duration_hours = compute_request.get("duration_hours", 1)
|
||||
|
||||
total_price = self.web3_config.task_price_eth * duration_hours
|
||||
|
||||
# Create compute offer
|
||||
offer = {
|
||||
"provider": self.address,
|
||||
"gpu": self.gpu_specs,
|
||||
"price_per_hour_eth": self.web3_config.task_price_eth,
|
||||
"total_price_eth": total_price,
|
||||
"tee_enabled": self.web3_config.tee_enabled,
|
||||
"min_duration_hours": 0.1,
|
||||
"max_duration_hours": 24
|
||||
}
|
||||
|
||||
# If TEE is requested, provide attestation
|
||||
if compute_request.get("require_tee", False) and self.confidential_agent:
|
||||
dummy_attestation = self.confidential_agent.tee_executor.get_attestation()
|
||||
offer["attestation"] = dummy_attestation.to_dict()
|
||||
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content=f"GPU compute available: {self.gpu_specs['model']} for {total_price} ETH",
|
||||
metadata={"compute_offer": offer}
|
||||
)
|
||||
@@ -0,0 +1,433 @@
|
||||
"""Web3-enabled network orchestration with deterministic execution."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from typing import TypeVar, Generic, List, Optional, Dict, Any, Union
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .network import Network
|
||||
from .state import State
|
||||
from .agent import Agent, InferenceResult
|
||||
from .router import Router, RouterFn
|
||||
from .web3_agent import Web3Agent, Web3AgentConfig
|
||||
from .wallet import generate_shared_mnemonic, derive_agent_wallet, WalletConfig
|
||||
from .marketplace import AgentMarketplace, ServiceType, marketplace
|
||||
|
||||
|
||||
S = TypeVar("S", bound=State)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkEconomics:
|
||||
"""Economic configuration for agent network."""
|
||||
|
||||
# Token economics
|
||||
network_token_symbol: str = "AI"
|
||||
initial_agent_balance: float = 1000.0 # In network tokens
|
||||
|
||||
# Fee structure
|
||||
network_fee_percent: float = 0.01 # 1% network fee
|
||||
min_task_fee: float = 0.001 # Minimum fee per task
|
||||
|
||||
# Incentives
|
||||
completion_bonus: float = 0.1 # Bonus for completing tasks
|
||||
quality_multiplier: float = 2.0 # Multiplier for high-quality work
|
||||
|
||||
# Slashing
|
||||
failure_penalty: float = 0.05 # Penalty for failed tasks
|
||||
timeout_penalty: float = 0.02 # Penalty for timeouts
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeterministicConfig:
|
||||
"""Configuration for deterministic execution."""
|
||||
|
||||
# Seed for randomness
|
||||
seed: int = 42
|
||||
|
||||
# Execution order
|
||||
enforce_order: bool = True
|
||||
allow_parallel: bool = False
|
||||
|
||||
# Reproducibility
|
||||
record_all_calls: bool = True
|
||||
verify_outputs: bool = True
|
||||
|
||||
# Checkpointing
|
||||
checkpoint_every_n_steps: int = 10
|
||||
checkpoint_on_completion: bool = True
|
||||
|
||||
|
||||
class Web3Network(Network[S], Generic[S]):
|
||||
"""Network with Web3 integration and deterministic execution."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
state: S,
|
||||
agents: List[Union[Agent[S], Web3Agent]],
|
||||
router: Union[Router, RouterFn[S]],
|
||||
shared_mnemonic: Optional[str] = None,
|
||||
network_economics: Optional[NetworkEconomics] = None,
|
||||
deterministic_config: Optional[DeterministicConfig] = None,
|
||||
marketplace: Optional[AgentMarketplace] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""Initialize Web3-enabled network.
|
||||
|
||||
Args:
|
||||
state: Initial state
|
||||
agents: List of agents (can be Web3Agent instances)
|
||||
router: Router for agent orchestration
|
||||
shared_mnemonic: Shared mnemonic for agent wallets
|
||||
network_economics: Economic configuration
|
||||
deterministic_config: Deterministic execution config
|
||||
marketplace: Agent marketplace instance
|
||||
**kwargs: Additional Network parameters
|
||||
"""
|
||||
super().__init__(
|
||||
state=state,
|
||||
agents=agents,
|
||||
router=router,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Web3 configuration
|
||||
self.shared_mnemonic = shared_mnemonic or generate_shared_mnemonic()
|
||||
self.network_economics = network_economics or NetworkEconomics()
|
||||
self.deterministic_config = deterministic_config or DeterministicConfig()
|
||||
self.marketplace = marketplace or globals()['marketplace']
|
||||
|
||||
# Initialize Web3 agents
|
||||
self._initialize_web3_agents()
|
||||
|
||||
# Economic tracking
|
||||
self.network_treasury = 0.0
|
||||
self.total_fees_collected = 0.0
|
||||
self.total_rewards_distributed = 0.0
|
||||
|
||||
# Deterministic execution tracking
|
||||
self.execution_log: List[Dict[str, Any]] = []
|
||||
self.execution_hash: Optional[str] = None
|
||||
|
||||
def _initialize_web3_agents(self):
|
||||
"""Initialize Web3 capabilities for agents."""
|
||||
for i, agent_class in enumerate(self.agents):
|
||||
# Skip if already instantiated
|
||||
if isinstance(agent_class, Agent):
|
||||
agent = agent_class
|
||||
else:
|
||||
# Instantiate agent
|
||||
agent = agent_class()
|
||||
|
||||
# If it's a Web3Agent, initialize wallet
|
||||
if isinstance(agent, Web3Agent):
|
||||
if not agent.wallet and agent.web3_config.wallet_enabled:
|
||||
# Derive wallet from shared mnemonic
|
||||
wallet = derive_agent_wallet(
|
||||
self.shared_mnemonic,
|
||||
agent_index=i,
|
||||
network_rpc=agent.web3_config.wallet_config.network_rpc
|
||||
if agent.web3_config.wallet_config else "mock://localhost"
|
||||
)
|
||||
agent.wallet = wallet
|
||||
|
||||
# Initial funding
|
||||
agent.earnings = self.network_economics.initial_agent_balance
|
||||
|
||||
# Store instance
|
||||
self._agent_instances[agent.name] = agent
|
||||
|
||||
async def run(self) -> S:
|
||||
"""Execute network with Web3 enhancements."""
|
||||
if self._running:
|
||||
raise RuntimeError("Network already running")
|
||||
|
||||
self._running = True
|
||||
start_time = time.time()
|
||||
|
||||
# Set deterministic seed
|
||||
import random
|
||||
import numpy as np
|
||||
random.seed(self.deterministic_config.seed)
|
||||
np.random.seed(self.deterministic_config.seed)
|
||||
|
||||
try:
|
||||
step = 0
|
||||
while self.call_count < self.max_steps:
|
||||
# Checkpoint if needed
|
||||
if (self.deterministic_config.checkpoint_every_n_steps > 0 and
|
||||
step % self.deterministic_config.checkpoint_every_n_steps == 0):
|
||||
await self._checkpoint(f"step_{step}")
|
||||
|
||||
# Route to next agent
|
||||
next_agent = await self._route()
|
||||
if next_agent is None:
|
||||
break
|
||||
|
||||
# Record pre-execution state
|
||||
pre_state = self.state.to_dict() if hasattr(self.state, 'to_dict') else str(self.state)
|
||||
|
||||
# Execute agent
|
||||
result = await self._execute_agent(next_agent)
|
||||
|
||||
# Record execution
|
||||
self._record_execution(next_agent, result, pre_state)
|
||||
|
||||
# Handle economics if Web3 agent
|
||||
if isinstance(next_agent, Web3Agent):
|
||||
await self._handle_agent_economics(next_agent, result)
|
||||
|
||||
# Check marketplace for matches
|
||||
await self._check_marketplace(next_agent, result)
|
||||
|
||||
step += 1
|
||||
|
||||
# Final checkpoint
|
||||
if self.deterministic_config.checkpoint_on_completion:
|
||||
await self._checkpoint("final")
|
||||
|
||||
# Compute execution hash
|
||||
self.execution_hash = self._compute_execution_hash()
|
||||
|
||||
# Log summary
|
||||
duration = time.time() - start_time
|
||||
print(f"\nNetwork execution completed:")
|
||||
print(f" Duration: {duration:.2f}s")
|
||||
print(f" Steps: {step}")
|
||||
print(f" Fees collected: {self.total_fees_collected:.4f}")
|
||||
print(f" Rewards distributed: {self.total_rewards_distributed:.4f}")
|
||||
print(f" Execution hash: {self.execution_hash}")
|
||||
|
||||
finally:
|
||||
self._running = False
|
||||
|
||||
return self.state
|
||||
|
||||
def _record_execution(self, agent: Agent, result: InferenceResult, pre_state: Any):
|
||||
"""Record execution for determinism."""
|
||||
if not self.deterministic_config.record_all_calls:
|
||||
return
|
||||
|
||||
record = {
|
||||
"step": len(self.execution_log),
|
||||
"timestamp": time.time(),
|
||||
"agent": agent.name,
|
||||
"pre_state": pre_state,
|
||||
"result": result.to_dict(),
|
||||
"post_state": self.state.to_dict() if hasattr(self.state, 'to_dict') else str(self.state)
|
||||
}
|
||||
|
||||
self.execution_log.append(record)
|
||||
|
||||
def _compute_execution_hash(self) -> str:
|
||||
"""Compute hash of entire execution for verification."""
|
||||
# Create deterministic representation
|
||||
execution_data = {
|
||||
"seed": self.deterministic_config.seed,
|
||||
"agents": [a.name for a in self._agent_instances.values()],
|
||||
"log": self.execution_log
|
||||
}
|
||||
|
||||
# Compute hash
|
||||
json_str = json.dumps(execution_data, sort_keys=True)
|
||||
return hashlib.sha256(json_str.encode()).hexdigest()
|
||||
|
||||
async def _handle_agent_economics(self, agent: Web3Agent, result: InferenceResult):
|
||||
"""Handle economic aspects of agent execution."""
|
||||
# Charge network fee
|
||||
task_fee = max(
|
||||
self.network_economics.min_task_fee,
|
||||
agent.web3_config.task_price_eth * self.network_economics.network_fee_percent
|
||||
)
|
||||
|
||||
if agent.balance_eth >= task_fee:
|
||||
# Deduct fee (in real implementation, this would be on-chain)
|
||||
agent.spending += task_fee
|
||||
self.network_treasury += task_fee
|
||||
self.total_fees_collected += task_fee
|
||||
|
||||
# Reward for completion
|
||||
if result.content and "error" not in result.content.lower():
|
||||
reward = self.network_economics.completion_bonus
|
||||
|
||||
# Quality bonus
|
||||
if result.metadata.get("quality_score", 0.5) > 0.8:
|
||||
reward *= self.network_economics.quality_multiplier
|
||||
|
||||
agent.earnings += reward
|
||||
self.total_rewards_distributed += reward
|
||||
|
||||
# Update reputation
|
||||
agent.update_reputation(0.1)
|
||||
else:
|
||||
# Penalty for failure
|
||||
penalty = self.network_economics.failure_penalty
|
||||
agent.spending += penalty
|
||||
agent.update_reputation(-0.1)
|
||||
|
||||
async def _check_marketplace(self, agent: Agent, result: InferenceResult):
|
||||
"""Check marketplace for service opportunities."""
|
||||
if not isinstance(agent, Web3Agent):
|
||||
return
|
||||
|
||||
# Check if agent advertised any services
|
||||
if "service_offer" in result.metadata:
|
||||
offer = result.metadata["service_offer"]
|
||||
offer_id = self.marketplace.post_offer(
|
||||
agent=agent,
|
||||
service_type=ServiceType(offer.get("type", "custom")),
|
||||
description=offer.get("description", ""),
|
||||
price_eth=offer.get("price_eth", 0.01),
|
||||
requires_tee=offer.get("requires_tee", False)
|
||||
)
|
||||
print(f"Agent {agent.name} posted offer: {offer_id}")
|
||||
|
||||
# Check if agent requested any services
|
||||
if "service_request" in result.metadata:
|
||||
request = result.metadata["service_request"]
|
||||
request_id = self.marketplace.post_request(
|
||||
agent=agent,
|
||||
service_type=ServiceType(request.get("type", "custom")),
|
||||
description=request.get("description", ""),
|
||||
max_price_eth=request.get("max_price_eth", 0.1),
|
||||
requires_tee=request.get("requires_tee", False)
|
||||
)
|
||||
print(f"Agent {agent.name} posted request: {request_id}")
|
||||
|
||||
async def _checkpoint(self, name: str):
|
||||
"""Create a checkpoint of current state."""
|
||||
if not self.checkpoint_dir:
|
||||
return
|
||||
|
||||
checkpoint = {
|
||||
"name": name,
|
||||
"timestamp": time.time(),
|
||||
"state": self.state.to_dict() if hasattr(self.state, 'to_dict') else str(self.state),
|
||||
"history": [entry.to_dict() for entry in self.history.entries],
|
||||
"economics": {
|
||||
"treasury": self.network_treasury,
|
||||
"fees_collected": self.total_fees_collected,
|
||||
"rewards_distributed": self.total_rewards_distributed
|
||||
},
|
||||
"execution_log": self.execution_log
|
||||
}
|
||||
|
||||
# Save checkpoint
|
||||
checkpoint_file = self.checkpoint_dir / f"checkpoint_{name}_{int(time.time())}.json"
|
||||
checkpoint_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(checkpoint_file, 'w') as f:
|
||||
json.dump(checkpoint, f, indent=2)
|
||||
|
||||
print(f"Checkpoint saved: {checkpoint_file}")
|
||||
|
||||
def verify_execution(self, other_hash: str) -> bool:
|
||||
"""Verify execution matches another run.
|
||||
|
||||
Args:
|
||||
other_hash: Execution hash from another run
|
||||
|
||||
Returns:
|
||||
True if executions match
|
||||
"""
|
||||
if not self.execution_hash:
|
||||
self.execution_hash = self._compute_execution_hash()
|
||||
|
||||
return self.execution_hash == other_hash
|
||||
|
||||
def get_agent_stats(self) -> Dict[str, Any]:
|
||||
"""Get statistics for all agents."""
|
||||
stats = {}
|
||||
|
||||
for name, agent in self._agent_instances.items():
|
||||
if isinstance(agent, Web3Agent):
|
||||
stats[name] = agent.get_stats()
|
||||
else:
|
||||
stats[name] = {
|
||||
"name": name,
|
||||
"type": "standard",
|
||||
"calls": sum(
|
||||
1 for entry in self.execution_log
|
||||
if entry["agent"] == name
|
||||
)
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def get_network_stats(self) -> Dict[str, Any]:
|
||||
"""Get network-wide statistics."""
|
||||
return {
|
||||
"agents": len(self._agent_instances),
|
||||
"total_steps": len(self.execution_log),
|
||||
"treasury_balance": self.network_treasury,
|
||||
"total_fees": self.total_fees_collected,
|
||||
"total_rewards": self.total_rewards_distributed,
|
||||
"marketplace_stats": self.marketplace.get_stats(),
|
||||
"execution_hash": self.execution_hash
|
||||
}
|
||||
|
||||
|
||||
# Factory function for easy network creation
|
||||
def create_web3_network(
|
||||
agents: List[Agent],
|
||||
task: str,
|
||||
enable_wallets: bool = True,
|
||||
enable_tee: bool = False,
|
||||
deterministic: bool = True
|
||||
) -> Web3Network[State]:
|
||||
"""Create a Web3-enabled network.
|
||||
|
||||
Args:
|
||||
agents: List of agents to include
|
||||
task: Initial task/query
|
||||
enable_wallets: Enable wallet functionality
|
||||
enable_tee: Enable TEE support
|
||||
deterministic: Enable deterministic execution
|
||||
|
||||
Returns:
|
||||
Configured Web3Network instance
|
||||
"""
|
||||
# Configure agents
|
||||
web3_agents = []
|
||||
for agent in agents:
|
||||
if isinstance(agent, Web3Agent):
|
||||
web3_agents.append(agent)
|
||||
else:
|
||||
# Wrap in Web3Agent
|
||||
config = Web3AgentConfig(
|
||||
wallet_enabled=enable_wallets,
|
||||
tee_enabled=enable_tee
|
||||
)
|
||||
|
||||
class Web3Wrapper(Web3Agent):
|
||||
async def _run_impl(self, state, history, network):
|
||||
# Delegate to original agent
|
||||
return await agent.run(state, history, network)
|
||||
|
||||
wrapped = Web3Wrapper(
|
||||
name=agent.name,
|
||||
description=getattr(agent, 'description', ''),
|
||||
web3_config=config
|
||||
)
|
||||
wrapped.tools = getattr(agent, 'tools', [])
|
||||
web3_agents.append(wrapped)
|
||||
|
||||
# Create initial state
|
||||
state = State()
|
||||
state["task"] = task
|
||||
state["start_time"] = time.time()
|
||||
|
||||
# Simple sequential router
|
||||
from .router import sequential_router
|
||||
router = sequential_router([a.name for a in web3_agents])
|
||||
|
||||
# Create network
|
||||
return Web3Network(
|
||||
state=state,
|
||||
agents=web3_agents,
|
||||
router=router,
|
||||
deterministic_config=DeterministicConfig() if deterministic else None
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hanzo-agents"
|
||||
version = "0.1.0"
|
||||
description = "Production-grade AI agent runtime for deterministic, debuggable agent systems"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Hanzo AI", email = "[email protected]"},
|
||||
]
|
||||
keywords = ["ai", "agents", "llm", "orchestration", "multi-agent"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic>=2.0",
|
||||
"typing-extensions>=4.5",
|
||||
"aiofiles>=23.0",
|
||||
"asyncio>=3.4",
|
||||
"structlog>=24.0",
|
||||
"prometheus-client>=0.19",
|
||||
"pyyaml>=6.0",
|
||||
"click>=8.0",
|
||||
"rich>=13.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
all = [
|
||||
"chromadb>=0.4",
|
||||
"neo4j>=5.0",
|
||||
"qdrant-client>=1.6",
|
||||
"faiss-cpu>=1.7",
|
||||
"duckdb>=0.9",
|
||||
"langchain>=0.1",
|
||||
"litellm>=1.0",
|
||||
"anthropic>=0.15",
|
||||
"openai>=1.0",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"pytest-asyncio>=0.21",
|
||||
"pytest-cov>=4.0",
|
||||
"pytest-mock>=3.0",
|
||||
"mypy>=1.0",
|
||||
"ruff>=0.1",
|
||||
"black>=23.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
hanzo-agents = "hanzo_agents.cli:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/hanzoai/agents"
|
||||
Documentation = "https://docs.hanzo.ai/agents"
|
||||
Repository = "https://github.com/hanzoai/agents"
|
||||
Issues = "https://github.com/hanzoai/agents/issues"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hanzo_agents"]
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Simple test of hanzo-agents with Web3 features."""
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
# Add hanzo_agents to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# Now import
|
||||
from hanzo_agents import (
|
||||
Web3Agent, Web3AgentConfig, State,
|
||||
generate_shared_mnemonic, WalletConfig,
|
||||
ServiceType, AgentMarketplace
|
||||
)
|
||||
|
||||
|
||||
async def test_basic_web3_agents():
|
||||
"""Test basic Web3 agent functionality."""
|
||||
print("Testing Hanzo Agents with Web3 Integration")
|
||||
print("=" * 50)
|
||||
|
||||
# Create shared mnemonic
|
||||
mnemonic = generate_shared_mnemonic()
|
||||
print(f"Generated mnemonic (first 3 words): {' '.join(mnemonic.split()[:3])}...")
|
||||
|
||||
# Create two agents
|
||||
print("\nCreating Web3-enabled agents...")
|
||||
|
||||
alice = Web3Agent(
|
||||
name="Alice",
|
||||
description="A helpful assistant",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
wallet_config=WalletConfig(
|
||||
mnemonic=mnemonic,
|
||||
account_index=0,
|
||||
network_rpc="mock://localhost"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
bob = Web3Agent(
|
||||
name="Bob",
|
||||
description="A technical expert",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(
|
||||
mnemonic=mnemonic,
|
||||
account_index=1,
|
||||
network_rpc="mock://localhost"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Give them some initial balance
|
||||
alice.earnings = 1.0
|
||||
bob.earnings = 0.5
|
||||
|
||||
print(f"\nAlice:")
|
||||
print(f" Address: {alice.address}")
|
||||
print(f" Balance: {alice.balance_eth} ETH")
|
||||
print(f" Has TEE: {alice.confidential_agent is not None}")
|
||||
|
||||
print(f"\nBob:")
|
||||
print(f" Address: {bob.address}")
|
||||
print(f" Balance: {bob.balance_eth} ETH")
|
||||
print(f" Has TEE: {bob.confidential_agent is not None}")
|
||||
|
||||
# Test payment
|
||||
print("\n--- Testing Payments ---")
|
||||
|
||||
# Bob requests payment from Alice
|
||||
payment_request = await bob.request_payment(
|
||||
from_address=alice.address,
|
||||
amount_eth=0.1,
|
||||
task_description="Technical consulting"
|
||||
)
|
||||
|
||||
print(f"Bob requested {payment_request['amount_eth']} ETH from Alice")
|
||||
|
||||
# Alice pays Bob
|
||||
tx = await alice.pay_agent(
|
||||
to_address=bob.address,
|
||||
amount_eth=0.1,
|
||||
reason="Payment for consulting"
|
||||
)
|
||||
|
||||
if tx:
|
||||
print(f"Alice sent payment: {tx.hash}")
|
||||
print(f"Alice new balance: {alice.balance_eth} ETH")
|
||||
print(f"Bob new balance: {bob.balance_eth} ETH")
|
||||
|
||||
# Test TEE execution
|
||||
print("\n--- Testing TEE Confidential Execution ---")
|
||||
|
||||
if alice.confidential_agent:
|
||||
code = """
|
||||
# Confidential computation
|
||||
result = {
|
||||
"computation": "sensitive",
|
||||
"value": inputs["x"] * inputs["y"],
|
||||
"secure": True
|
||||
}
|
||||
"""
|
||||
|
||||
result = await alice.execute_confidential(
|
||||
code,
|
||||
{"x": 42, "y": 2}
|
||||
)
|
||||
|
||||
print(f"TEE computation result: {result['result']['value']}")
|
||||
print(f"Attestation available: {'attestation' in result}")
|
||||
|
||||
# Test marketplace
|
||||
print("\n--- Testing Marketplace ---")
|
||||
|
||||
marketplace = AgentMarketplace()
|
||||
|
||||
# Alice offers a service
|
||||
offer_id = marketplace.post_offer(
|
||||
agent=alice,
|
||||
service_type=ServiceType.CUSTOM,
|
||||
description="AI assistance and code review",
|
||||
price_eth=0.05
|
||||
)
|
||||
|
||||
print(f"Alice posted offer: {offer_id}")
|
||||
|
||||
# Bob requests a service
|
||||
request_id = marketplace.post_request(
|
||||
agent=bob,
|
||||
service_type=ServiceType.CUSTOM,
|
||||
description="Need help with code review",
|
||||
max_price_eth=0.1
|
||||
)
|
||||
|
||||
print(f"Bob posted request: {request_id}")
|
||||
|
||||
# Check matches
|
||||
if marketplace.matches:
|
||||
match = list(marketplace.matches.values())[0]
|
||||
print(f"Match found! {match.offer.agent_name} <-> {match.request.requester_name}")
|
||||
|
||||
# Test agent stats
|
||||
print("\n--- Agent Statistics ---")
|
||||
|
||||
alice_stats = alice.get_stats()
|
||||
bob_stats = bob.get_stats()
|
||||
|
||||
print(f"\nAlice stats:")
|
||||
for key, value in alice_stats.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print(f"\nBob stats:")
|
||||
for key, value in bob_stats.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n✅ All tests completed successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_basic_web3_agents())
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Simple test of Web3 agent features without full dependencies."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# Test basic imports
|
||||
print("Testing basic Web3 agent components...")
|
||||
|
||||
try:
|
||||
from hanzo_agents.core.wallet import (
|
||||
generate_shared_mnemonic,
|
||||
derive_agent_wallet,
|
||||
WalletConfig,
|
||||
AgentWallet
|
||||
)
|
||||
print("✓ Wallet imports successful")
|
||||
|
||||
# Test mnemonic generation
|
||||
mnemonic = generate_shared_mnemonic()
|
||||
print(f"✓ Generated mnemonic: {' '.join(mnemonic.split()[:3])}...")
|
||||
|
||||
# Test wallet derivation
|
||||
wallet = derive_agent_wallet(mnemonic, 0)
|
||||
print(f"✓ Derived wallet address: {wallet.address}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"✗ Wallet import failed: {e}")
|
||||
|
||||
try:
|
||||
from hanzo_agents.core.tee import (
|
||||
TEEProvider,
|
||||
TEEConfig,
|
||||
AttestationReport,
|
||||
MockTEEExecutor
|
||||
)
|
||||
print("\n✓ TEE imports successful")
|
||||
|
||||
# Test TEE config
|
||||
tee_config = TEEConfig(provider=TEEProvider.MOCK)
|
||||
print(f"✓ TEE config created: {tee_config.provider.value}")
|
||||
|
||||
# Test mock executor
|
||||
executor = MockTEEExecutor()
|
||||
result = executor.execute("test_code", {"x": 1})
|
||||
print(f"✓ Mock TEE execution: {result['success']}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"\n✗ TEE import failed: {e}")
|
||||
|
||||
try:
|
||||
from hanzo_agents.core.marketplace import (
|
||||
AgentMarketplace,
|
||||
ServiceType,
|
||||
ServiceOffer,
|
||||
ServiceRequest
|
||||
)
|
||||
print("\n✓ Marketplace imports successful")
|
||||
|
||||
# Test marketplace
|
||||
marketplace = AgentMarketplace()
|
||||
print(f"✓ Marketplace created")
|
||||
|
||||
# Test service types
|
||||
print(f"✓ Service types: {[s.value for s in ServiceType]}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"\n✗ Marketplace import failed: {e}")
|
||||
|
||||
# Test Web3Agent mock
|
||||
try:
|
||||
from hanzo_agents.core.web3_agent import Web3Agent, Web3AgentConfig
|
||||
print("\n✓ Web3Agent imports successful")
|
||||
|
||||
# Create a simple agent
|
||||
config = Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
tee_enabled=True,
|
||||
task_price_eth=0.01
|
||||
)
|
||||
|
||||
agent = Web3Agent(
|
||||
name="TestAgent",
|
||||
description="A test agent",
|
||||
web3_config=config
|
||||
)
|
||||
|
||||
print(f"✓ Created agent: {agent.name}")
|
||||
print(f"✓ Wallet enabled: {agent.wallet is not None}")
|
||||
print(f"✓ TEE enabled: {agent.confidential_agent is not None}")
|
||||
print(f"✓ Address: {agent.address[:10]}...")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"\n✗ Web3Agent import failed: {e}")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("Summary: Core Web3 components are working!")
|
||||
print("Note: Full examples require additional dependencies like structlog")
|
||||
print("\nTo run full examples, install dependencies:")
|
||||
print(" pip install structlog prometheus-client")
|
||||
print("="*50)
|
||||
@@ -0,0 +1,329 @@
|
||||
"""Test all agent example demos to ensure they work correctly."""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add examples directory to path
|
||||
examples_dir = Path(__file__).parent.parent / "examples"
|
||||
sys.path.insert(0, str(examples_dir))
|
||||
|
||||
# Import demo modules
|
||||
try:
|
||||
from startup_team_demo import run_startup_team_demo
|
||||
from agent_chat_demo import run_agent_chat_demo
|
||||
from research_team_demo import run_research_team_demo
|
||||
from game_dev_team_demo import run_game_dev_demo
|
||||
DEMOS_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
print(f"Failed to import demos: {e}")
|
||||
DEMOS_AVAILABLE = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(not DEMOS_AVAILABLE, reason="Demo modules not available")
|
||||
class TestAgentDemos:
|
||||
"""Test all agent demonstration examples."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_startup_team_demo(self, capsys):
|
||||
"""Test startup team collaboration demo."""
|
||||
# Run demo
|
||||
await run_startup_team_demo()
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = captured.out
|
||||
|
||||
# Verify key elements
|
||||
assert "Starting Startup Team Demo" in output
|
||||
assert "architect" in output.lower()
|
||||
assert "product_manager" in output.lower()
|
||||
assert "designer" in output.lower()
|
||||
assert "marketer" in output.lower()
|
||||
assert "engineer" in output.lower()
|
||||
|
||||
# Check for completion
|
||||
assert "Project Summary" in output
|
||||
assert "Team Economics" in output
|
||||
assert "Execution hash" in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_chat_demo(self, capsys):
|
||||
"""Test agent chat conversation demo."""
|
||||
# Run demo
|
||||
await run_agent_chat_demo()
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = captured.out
|
||||
|
||||
# Verify conversation elements
|
||||
assert "Agent Chat Demo" in output
|
||||
assert "Alice" in output
|
||||
assert "Bob" in output
|
||||
assert "typing..." in output
|
||||
|
||||
# Check for system info exchange
|
||||
assert "Python" in output
|
||||
assert "model" in output.lower()
|
||||
assert "capabilities" in output.lower()
|
||||
|
||||
# Verify completion
|
||||
assert "Conversation Summary" in output
|
||||
assert "Agent Stats" in output
|
||||
assert "lovely chat" in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_research_team_demo(self, capsys):
|
||||
"""Test research team collaboration demo."""
|
||||
# Run demo
|
||||
await run_research_team_demo()
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = captured.out
|
||||
|
||||
# Verify research workflow
|
||||
assert "Research Team Demo" in output
|
||||
assert "data_scientist" in output.lower()
|
||||
assert "research_analyst" in output.lower()
|
||||
assert "ml_engineer" in output.lower()
|
||||
assert "technical_writer" in output.lower()
|
||||
|
||||
# Check research outputs
|
||||
assert "Data Analysis" in output
|
||||
assert "Literature Review" in output
|
||||
assert "Model Performance" in output
|
||||
assert "Experiments" in output
|
||||
assert "Deliverables" in output
|
||||
|
||||
# Verify completion
|
||||
assert "Research project completed successfully" in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_game_dev_demo(self, capsys):
|
||||
"""Test game development team demo."""
|
||||
# Run demo
|
||||
await run_game_dev_demo()
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = captured.out
|
||||
|
||||
# Verify game dev workflow
|
||||
assert "Game Development Team Demo" in output
|
||||
assert "game_designer" in output.lower()
|
||||
assert "artist" in output.lower()
|
||||
assert "programmer" in output.lower()
|
||||
assert "sound_designer" in output.lower()
|
||||
assert "qa_tester" in output.lower()
|
||||
|
||||
# Check game details
|
||||
assert "Quantum Maze Runner" in output
|
||||
assert "Development Summary" in output
|
||||
assert "bugs found and fixed" in output
|
||||
|
||||
# Verify completion
|
||||
assert "Game ready for launch" in output
|
||||
|
||||
|
||||
@pytest.mark.skipif(not DEMOS_AVAILABLE, reason="Demo modules not available")
|
||||
class TestAgentInteractions:
|
||||
"""Test specific agent interaction patterns."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web3_payments(self):
|
||||
"""Test Web3 payment functionality between agents."""
|
||||
from hanzo_agents import Web3Agent, Web3AgentConfig, WalletConfig, State
|
||||
|
||||
# Create two agents
|
||||
agent1 = Web3Agent(
|
||||
name="payer",
|
||||
description="Agent that pays",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(network_rpc="mock://localhost")
|
||||
)
|
||||
)
|
||||
|
||||
agent2 = Web3Agent(
|
||||
name="receiver",
|
||||
description="Agent that receives",
|
||||
web3_config=Web3AgentConfig(
|
||||
wallet_enabled=True,
|
||||
wallet_config=WalletConfig(network_rpc="mock://localhost")
|
||||
)
|
||||
)
|
||||
|
||||
# Give agent1 balance
|
||||
agent1.earnings = 1.0
|
||||
|
||||
# Test payment request
|
||||
payment_request = await agent2.request_payment(
|
||||
from_address=agent1.address,
|
||||
amount_eth=0.1,
|
||||
task_description="Test service"
|
||||
)
|
||||
|
||||
assert payment_request["to"] == agent2.address
|
||||
assert payment_request["amount_eth"] == 0.1
|
||||
|
||||
# Test payment execution
|
||||
tx = await agent1.pay_agent(
|
||||
to_address=agent2.address,
|
||||
amount_eth=0.1,
|
||||
reason="Test payment"
|
||||
)
|
||||
|
||||
assert tx is not None
|
||||
assert agent1.spending == 0.1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tee_execution(self):
|
||||
"""Test TEE confidential execution."""
|
||||
from hanzo_agents import Web3Agent, Web3AgentConfig
|
||||
|
||||
agent = Web3Agent(
|
||||
name="secure_agent",
|
||||
description="Agent with TEE",
|
||||
web3_config=Web3AgentConfig(tee_enabled=True)
|
||||
)
|
||||
|
||||
# Test confidential execution
|
||||
code = """
|
||||
result = {"sum": inputs["a"] + inputs["b"]}
|
||||
"""
|
||||
|
||||
result = await agent.execute_confidential(
|
||||
code,
|
||||
{"a": 10, "b": 20}
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["result"]["sum"] == 30
|
||||
assert "attestation" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_marketplace_matching(self):
|
||||
"""Test marketplace service matching."""
|
||||
from hanzo_agents import (
|
||||
Web3Agent, Web3AgentConfig,
|
||||
AgentMarketplace, ServiceType
|
||||
)
|
||||
|
||||
marketplace = AgentMarketplace()
|
||||
|
||||
# Create provider
|
||||
provider = Web3Agent(
|
||||
name="provider",
|
||||
description="Service provider",
|
||||
web3_config=Web3AgentConfig(wallet_enabled=True)
|
||||
)
|
||||
|
||||
# Create requester
|
||||
requester = Web3Agent(
|
||||
name="requester",
|
||||
description="Service requester",
|
||||
web3_config=Web3AgentConfig(wallet_enabled=True)
|
||||
)
|
||||
|
||||
# Post offer
|
||||
offer_id = marketplace.post_offer(
|
||||
agent=provider,
|
||||
service_type=ServiceType.COMPUTE,
|
||||
description="GPU compute",
|
||||
price_eth=0.1
|
||||
)
|
||||
|
||||
# Post matching request
|
||||
request_id = marketplace.post_request(
|
||||
agent=requester,
|
||||
service_type=ServiceType.COMPUTE,
|
||||
description="Need GPU",
|
||||
max_price_eth=0.2
|
||||
)
|
||||
|
||||
# Should auto-match
|
||||
assert len(marketplace.matches) == 1
|
||||
match = list(marketplace.matches.values())[0]
|
||||
assert match.offer.agent_name == "provider"
|
||||
assert match.request.requester_name == "requester"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deterministic_execution(self):
|
||||
"""Test deterministic network execution."""
|
||||
from hanzo_agents import create_web3_network, Agent, State, InferenceResult
|
||||
|
||||
# Create simple test agents
|
||||
class TestAgent1(Agent):
|
||||
name = "test1"
|
||||
async def run(self, state, history, network):
|
||||
state["step1"] = "done"
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content="Step 1 complete"
|
||||
)
|
||||
|
||||
class TestAgent2(Agent):
|
||||
name = "test2"
|
||||
async def run(self, state, history, network):
|
||||
state["step2"] = "done"
|
||||
return InferenceResult(
|
||||
agent=self.name,
|
||||
content="Step 2 complete"
|
||||
)
|
||||
|
||||
# Create two identical networks
|
||||
agents = [TestAgent1(), TestAgent2()]
|
||||
|
||||
network1 = create_web3_network(
|
||||
agents=agents,
|
||||
task="Test determinism",
|
||||
deterministic=True
|
||||
)
|
||||
|
||||
network2 = create_web3_network(
|
||||
agents=agents,
|
||||
task="Test determinism",
|
||||
deterministic=True
|
||||
)
|
||||
|
||||
# Run both networks
|
||||
state1 = await network1.run()
|
||||
state2 = await network2.run()
|
||||
|
||||
# Should produce identical results
|
||||
assert network1.execution_hash == network2.execution_hash
|
||||
assert network1.verify_execution(network2.execution_hash)
|
||||
assert state1["step1"] == state2["step1"]
|
||||
assert state1["step2"] == state2["step2"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run specific demo if provided
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
demo_name = sys.argv[1]
|
||||
if demo_name == "startup":
|
||||
asyncio.run(run_startup_team_demo())
|
||||
elif demo_name == "chat":
|
||||
asyncio.run(run_agent_chat_demo())
|
||||
elif demo_name == "research":
|
||||
asyncio.run(run_research_team_demo())
|
||||
elif demo_name == "game":
|
||||
asyncio.run(run_game_dev_demo())
|
||||
else:
|
||||
print(f"Unknown demo: {demo_name}")
|
||||
print("Available demos: startup, chat, research, game")
|
||||
else:
|
||||
# Run all demos
|
||||
print("Running all demos...\n")
|
||||
asyncio.run(run_startup_team_demo())
|
||||
print("\n" + "="*80 + "\n")
|
||||
asyncio.run(run_agent_chat_demo())
|
||||
print("\n" + "="*80 + "\n")
|
||||
asyncio.run(run_research_team_demo())
|
||||
print("\n" + "="*80 + "\n")
|
||||
asyncio.run(run_game_dev_demo())
|
||||
@@ -0,0 +1,29 @@
|
||||
# hanzo-cli
|
||||
|
||||
This is an alias package for the Hanzo AI CLI. It installs the `hanzo` command-line tool.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hanzo-cli
|
||||
```
|
||||
|
||||
This will install the `hanzo` command with all dependencies.
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, use the `hanzo` command:
|
||||
|
||||
```bash
|
||||
hanzo --help
|
||||
hanzo chat
|
||||
hanzo serve
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
See the main documentation at https://docs.hanzo.ai/cli
|
||||
|
||||
## Note
|
||||
|
||||
This package is identical to installing `pip install hanzo[all]`. It exists for convenience and discoverability.
|
||||
@@ -0,0 +1,41 @@
|
||||
[project]
|
||||
name = "hanzo-cli-wrapper"
|
||||
version = "0.1.0"
|
||||
description = "Hanzo AI CLI - Alias package for 'hanzo' command"
|
||||
authors = [
|
||||
{name = "Hanzo AI", email = "[email protected]"},
|
||||
]
|
||||
dependencies = [
|
||||
"hanzo[all]>=0.1.0",
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.8"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://hanzo.ai"
|
||||
Repository = "https://github.com/hanzoai/python-sdk"
|
||||
Documentation = "https://docs.hanzo.ai/cli"
|
||||
|
||||
[project.keywords]
|
||||
keywords = ["ai", "cli", "hanzo", "agents", "llm", "mcp"]
|
||||
|
||||
[project.classifiers]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user