Update python monorepo

This commit is contained in:
2025-07-27 03:02:44 -05:00
parent 85cb04d4b3
commit a76b6365ab
821 changed files with 119229 additions and 1171 deletions
+1 -1
View File
@@ -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
+214
View File
@@ -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)"
+5 -9
View File
@@ -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`
+213 -195
View File
@@ -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>
+223
View File
@@ -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
+93
View File
@@ -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())
+1 -1
View File
@@ -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
+21
View File
@@ -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.
+60
View File
@@ -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)"
+67
View File
@@ -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.
+100
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
from .editor import file_editor
from .editor.file_cache import FileCache
__all__ = ['file_editor', 'FileCache']
+76
View File
@@ -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}>'
)
+2
View File
@@ -0,0 +1,2 @@
MAX_RESPONSE_LEN_CHAR: int = 16000
SNIPPET_CONTEXT_WINDOW: int = 4
+659
View File
@@ -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'
+115
View File
@@ -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)
+146
View File
@@ -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)
+119
View File
@@ -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
+5
View File
@@ -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>'
+49
View File
@@ -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
)
+9
View File
@@ -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']
+79
View File
@@ -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])
+122
View File
@@ -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
+41
View File
@@ -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
+28
View File
@@ -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')
+67
View File
@@ -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
+9
View File
@@ -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
+26
View File
@@ -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"
+101
View File
@@ -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
+2730
View File
File diff suppressed because it is too large Load Diff
+59
View File
@@ -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"
+2
View File
@@ -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)
+142
View File
@@ -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
+180
View File
@@ -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.
+240
View File
@@ -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
+80
View File
@@ -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",
]
+211
View File
@@ -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}")
+182
View File
@@ -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
+145
View File
@@ -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
+411
View File
@@ -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()
+234
View File
@@ -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
)
+69
View File
@@ -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"]
+164
View File
@@ -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())
+103
View File
@@ -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())
+29
View File
@@ -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.
+41
View File
@@ -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