diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4f4ac0c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github +.gitignore +*.md +!README.md +.env +.env.* +Dockerfile +.dockerignore +docs/ +deploy/ +api/ +tests/ +__pycache__/ +*.pyc +.venv/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..78818dc --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# hawk-sdk-python environment variables — copy to .env and fill in +# The hawk daemon must be running locally before using this SDK. +HAWK_BASE_URL=http://127.0.0.1:4590 +HAWK_API_KEY= diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..31bd212 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,58 @@ +name: Docker + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + paths: + - "Dockerfile" + - "src/**" + - "pyproject.toml" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: graycodeai/hawk-sdk-python + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ccb20da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim AS builder + +WORKDIR /build +COPY pyproject.toml VERSION README.md ./ +RUN pip install --no-cache-dir build + +COPY src/ src/ +RUN python -m build --wheel + +FROM python:3.12-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates tini && \ + rm -rf /var/lib/apt/lists/* && \ + adduser --disabled-password --gecos "" --uid 1000 hawk + +COPY --from=builder /build/dist/*.whl /tmp/ +RUN pip install --no-cache-dir /tmp/*.whl && rm -rf /tmp/*.whl + +USER hawk +WORKDIR /workspace +ENTRYPOINT ["tini", "--"] +CMD ["sleep", "infinity"] diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..00dbda6 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,47 @@ +openapi: "3.1.0" +info: + title: hawk-sdk-python — Python SDK API Reference + description: | + Python SDK for the hawk daemon HTTP API. Provides sync and async clients, + SSE streaming, tool decorator, and workflow builder. + + This is a client SDK — it connects to the hawk daemon at http://localhost:4590. + See hawk's api/openapi.yaml for the server-side contract. + version: "0.1.0" + license: + name: MIT + url: https://github.com/GrayCodeAI/hawk-sdk-python/blob/main/LICENSE + contact: + url: https://github.com/GrayCodeAI/hawk-sdk-python + +servers: + - url: http://localhost:4590 + description: Hawk daemon (managed by hawk, not this SDK) + +x-sdk-api: + package: hawk + classes: + - name: HawkClient + description: Synchronous client + constructor_args: + base_url: string + api_key: string + methods: + - health() + - chat(message, **kwargs) + - chat_stream(message, **kwargs) + - list_sessions(limit, offset) + - get_session(session_id) + - delete_session(session_id) + - get_session_messages(session_id, limit, offset) + - stats() + - name: AsyncHawkClient + description: Asynchronous client (same methods with async/await) + - name: Agent + description: Higher-level conversation agent with session history + - name: Workflow + description: Multi-step workflow builder with retry config + decorators: + - name: tool + description: Decorator to register a function as a hawk tool + usage: "@tool()" diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..d215dee --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,11 @@ +name: hawk-sdk-python + +services: + hawk-sdk-python: + build: + context: ../../ + dockerfile: Dockerfile + image: ghcr.io/graycodeai/hawk-sdk-python:dev + env_file: + - path: ../../.env.example + required: false diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..8f52b50 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,120 @@ +
+ +# 🐍 hawk-sdk-python Architecture + +**Python SDK for the Hawk Daemon API** + +[![Python](https://img.shields.io/badge/Python-3.10+-3776AB?logo=python)](https://python.org/) +[![Type](https://img.shields.io/badge/Type-SDK-blue)]() + +
+ +--- + +## 🎯 Overview + +Idiomatic Python client for the hawk daemon HTTP API. Provides both **sync** and **async** clients, SSE streaming, a tool decorator, agent abstraction, and workflow builder. Uses `httpx` for HTTP transport and `Pydantic` for response models. + +--- + +## 🧱 Modules + +``` +src/hawk/ +├── __init__.py 📤 Public exports +├── client.py 🔌 HawkClient (sync) + AsyncHawkClient (async) +├── types.py 📋 Pydantic models (ChatResponse, Session, Stats) +├── errors.py ❌ HawkAPIError base + subclasses, parse_error() +├── retry.py 🔄 RetryConfig, with_retry_sync(), with_retry() +├── streaming.py 📡 StreamReader, AsyncStreamReader, StreamEvent +├── agent.py 🤖 Agent (conversation history, async support) +├── tools.py 🛠️ @tool() decorator, chat_with_tools() +├── workflow.py 🔧 Workflow builder +├── discovery.py 🔍 Auto-discover running hawk daemon on localhost +├── memory_tools.py 🧠 Memory graph operations (yaad integration) +├── evaluate.py 📊 Evaluation helpers +└── tracing.py 📈 OpenTelemetry tracing support +``` + +--- + +## 📤 Client Usage + +```python +from hawk import HawkClient, AsyncHawkClient + +# 🔌 Sync client +with HawkClient(base_url="http://localhost:4590", api_key="sk-...") as client: + health = client.health() + response = client.chat("list files in src/") + print(response.response) + +# 📡 Async client +async with AsyncHawkClient() as client: + async for event in client.chat_stream("explain this code"): + print(event.data, end="", flush=True) + +# 📋 Sessions +sessions = client.list_sessions(limit=10) +msgs = client.get_session_messages(session_id) +client.delete_session(session_id) +``` + +--- + +## 🛠️ Tool Decorator + +```python +from hawk import tool, HawkClient + +@tool() +def read_file(path: str) -> str: + with open(path) as f: + return f.read() + +with HawkClient() as client: + response = client.chat_with_tools("read config.json", tools=[read_file]) +``` + +--- + +## 🤖 Agent (Higher-Level) + +```python +from hawk import Agent + +agent = Agent(base_url="http://localhost:4590") +resp1 = agent.chat("refactor this function") +resp2 = agent.chat("now add type hints") # continues same session +``` + +--- + +## ❌ Error Handling + +```python +from hawk.errors import NotFoundError, RateLimitError + +try: + response = client.chat("...") +except RateLimitError as e: + time.sleep(e.retry_after or 1) +except NotFoundError: + ... +``` + +| Error Class | HTTP Status | +|-------------|:-----------:| +| `NotFoundError` | 404 | +| `RateLimitError` | 429 | +| `InternalServerError` | 500 | + +--- + +## 🔄 Retry & Streaming + +| Feature | Behavior | +|---------|----------| +| **Auto-retry** | 429, 500, 502, 503, 504 with exponential backoff + jitter | +| **Retry-After: 0** | Valid — don't retry immediately | +| **Dual client** | Every method on both `HawkClient` and `AsyncHawkClient` |