Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
.github
.gitignore
*.md
!README.md
.env
.env.*
Dockerfile
.dockerignore
docs/
deploy/
api/
tests/
__pycache__/
*.pyc
.venv/
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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=
58 changes: 58 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
47 changes: 47 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -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()"
11 changes: 11 additions & 0 deletions deploy/docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
120 changes: 120 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<div align="center">

# 🐍 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)]()

</div>

---

## 🎯 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` |
24 changes: 24 additions & 0 deletions examples/basic/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Basic example of using the Hawk SDK."""

from hawk import HawkClient

def main():
with HawkClient() as client:
# Health check
health = client.health()
print(f"Hawk daemon: version={health.version}, sessions={health.sessions}")

# Chat
response = client.chat("Explain what a decorator is in Python")
print(f"Response: {response.response}")

# Streaming
print("\nStreaming response:")
with client.chat_stream("Write a haiku about code") as stream:
for event in stream.events():
print(event.data, end="", flush=True)
print()

if __name__ == "__main__":
main()
Loading