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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Run linting
run: |
ruff check .
black --check .
ruff format --check .

- name: Run type checking
run: mypy openintent --ignore-missing-imports
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ All notable changes to the OpenIntent SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.14.1] - 2026-02-27

### Fixed

- **SDK/Server field format mismatches** — Fixed three category of mismatches between the Python SDK client and the protocol server's Pydantic models:
- **`constraints` type mismatch** — `create_intent()`, `create_child_intent()`, and `IntentSpec` sent `constraints` as a `list[str]` (e.g., `["rule1", "rule2"]`), but the server expects `Dict[str, Any]` (e.g., `{"rules": [...]}`). All three methods (sync and async) now accept and send `dict[str, Any]`. `Intent.from_dict()` retains backward compatibility for legacy list-format constraints.
- **`createdBy` → `created_by` for portfolios** — `create_portfolio()` sent `createdBy` and `governancePolicy` (camelCase) but the server's `PortfolioCreate` model expects `created_by` and `governance_policy` (snake_case). Fixed in both sync and async clients.
- **`get_portfolio_intents()` response parsing** — The server returns a raw JSON array from `GET /api/v1/portfolios/{id}/intents`, but the SDK expected a `{"intents": [...]}` wrapper dict, silently returning an empty list.

- **Silent empty results from list endpoints** — Seven additional list-returning methods used `data.get("key", [])` which silently returned empty lists when the server sent raw JSON arrays. All now use `isinstance(data, list)` detection to handle both raw array and wrapped dict responses:
- `list_portfolios()` — expected `{"portfolios": [...]}`
- `get_intent_portfolios()` — expected `{"portfolios": [...]}`
- `get_attachments()` — expected `{"attachments": [...]}`
- `get_costs()` — expected `{"costs": [], "summary": {}}`
- `get_failures()` — expected `{"failures": [...]}`
- `get_subscriptions()` — expected `{"subscriptions": [...]}`
- `federation_list_agents()` — expected `{"agents": [...]}`

- **`IntentLease.from_dict()` KeyError on server responses** — `acquire_lease()` threw `KeyError('status')` because the server's `LeaseResponse` model does not include a `status` field (it uses `acquired_at`, `expires_at`, and `released_at` to represent lease state). `IntentLease.from_dict()` now derives status from these fields: `RELEASED` if `released_at` is set, `EXPIRED` if `expires_at` is in the past, otherwise `ACTIVE`. Also handles the field name difference `acquired_at` (server) vs `created_at` (SDK). Backward compatible with the SDK's own serialization format.

- **Stale database singleton after server restart** — `get_database()` cached the `Database` instance at module level and never checked whether `database_url` changed between calls. When the protocol server restarted on a different port (e.g., `openintent_server_8001.db` → `openintent_server_8002.db`), the singleton kept pointing at the old file. Writes went to the old database; reads came from the new (empty) one — intents appeared created but were invisible to `list_intents`. The singleton now tracks its URL and recreates the connection when the URL changes.

- **Example and test updates** — All examples (`basic_usage.py`, `openai_multi_agent.py`, `multi_agent/coordinator.py`, `compliance_review/coordinator.py`) and tests updated to use dict-format constraints.

### Changed

- All version references updated to 0.14.1 across Python SDK, MCP server package, and changelog.

---

## [0.14.0] - 2026-02-25

### Added
Expand Down
13 changes: 10 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@ Thank you for your interest in contributing to the OpenIntent Python SDK! This d

We use the following tools for code quality:

- **Black** for code formatting
- **Ruff** for linting
- **Ruff** for linting and code formatting
- **mypy** for type checking
- **pre-commit** for automatic formatting on every commit

Set up pre-commit hooks (runs automatically with `make install-dev`):

```bash
pip install pre-commit
pre-commit install
```

Before submitting a PR, run:

```bash
black openintent/
ruff format openintent/
ruff check openintent/ --fix
mypy openintent/
```
Expand Down
113 changes: 57 additions & 56 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,89 +8,90 @@ PORT ?= 8000
MCP_ROLE ?= reader

help: ## Show this help message
@echo "OpenIntent SDK — available targets:"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Variables (override with VAR=value):"
@echo " PORT Server port (default: $(PORT))"
@echo " MCP_ROLE MCP server role (default: $(MCP_ROLE))"
@echo " PYTHON Python binary (default: $(PYTHON))"
@echo "OpenIntent SDK — available targets:"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Variables (override with VAR=value):"
@echo " PORT Server port (default: $(PORT))"
@echo " MCP_ROLE MCP server role (default: $(MCP_ROLE))"
@echo " PYTHON Python binary (default: $(PYTHON))"

install: ## Install SDK with server + all adapters
$(PIP) install -e ".[server,all-adapters]"
$(PIP) install -e ".[server,all-adapters]"

install-dev: ## Install SDK with dev + server + all extras
$(PIP) install -e ".[dev,server,all-adapters]"
$(PIP) install -e ".[dev,server,all-adapters]"
@if command -v pre-commit >/dev/null 2>&1; then pre-commit install; fi

install-all: ## Install everything including MCP dependencies
$(PIP) install -e ".[dev,server,all-adapters]"
$(PIP) install mcp
npm install -g @openintentai/mcp-server
$(PIP) install -e ".[dev,server,all-adapters]"
$(PIP) install mcp
npm install -g @openintentai/mcp-server

server: ## Start the OpenIntent server
openintent-server --port $(PORT)
openintent-server --port $(PORT)

test: ## Run the full test suite
$(PYTHON) -m pytest tests/ -v
$(PYTHON) -m pytest tests/ -v

test-quick: ## Run tests without slow markers
$(PYTHON) -m pytest tests/ -v -m "not slow"
$(PYTHON) -m pytest tests/ -v -m "not slow"

test-mcp: ## Run MCP-related tests only
$(PYTHON) -m pytest tests/ -v -k "mcp"
$(PYTHON) -m pytest tests/ -v -k "mcp"

lint: ## Run linter + formatter check + type checker
ruff check openintent/
black --check openintent/
mypy openintent/
ruff check openintent/
ruff format --check openintent/
mypy openintent/

format: ## Auto-format code
black openintent/ tests/
ruff check --fix openintent/
ruff format openintent/ tests/ examples/
ruff check --fix openintent/

typecheck: ## Run type checker only
mypy openintent/
mypy openintent/

setup-mcp: ## Install MCP dependencies (Python + Node)
@echo "Installing Python MCP SDK..."
$(PIP) install mcp
@echo "Installing OpenIntent MCP server (Node)..."
npm install -g @openintentai/mcp-server
@echo ""
@echo "MCP setup complete. Next steps:"
@echo " make server — Start the OpenIntent server"
@echo " make mcp-server — Start the MCP server (in another terminal)"
@echo " make full-stack — Start both together"
@echo "Installing Python MCP SDK..."
$(PIP) install mcp
@echo "Installing OpenIntent MCP server (Node)..."
npm install -g @openintentai/mcp-server
@echo ""
@echo "MCP setup complete. Next steps:"
@echo " make server — Start the OpenIntent server"
@echo " make mcp-server — Start the MCP server (in another terminal)"
@echo " make full-stack — Start both together"

mcp-server: ## Start the MCP server (connects to local OpenIntent server)
OPENINTENT_SERVER_URL=http://localhost:$(PORT) \
OPENINTENT_API_KEY=dev-user-key \
OPENINTENT_MCP_ROLE=$(MCP_ROLE) \
$(NPX) -y @openintentai/mcp-server
OPENINTENT_SERVER_URL=http://localhost:$(PORT) \
OPENINTENT_API_KEY=dev-user-key \
OPENINTENT_MCP_ROLE=$(MCP_ROLE) \
$(NPX) -y @openintentai/mcp-server

full-stack: ## Start OpenIntent server + MCP server together
@echo "Starting OpenIntent server on port $(PORT)..."
@openintent-server --port $(PORT) &
@sleep 2
@echo "Starting MCP server (role: $(MCP_ROLE))..."
@OPENINTENT_SERVER_URL=http://localhost:$(PORT) \
OPENINTENT_API_KEY=dev-user-key \
OPENINTENT_MCP_ROLE=$(MCP_ROLE) \
$(NPX) -y @openintentai/mcp-server
@echo "Starting OpenIntent server on port $(PORT)..."
@openintent-server --port $(PORT) &
@sleep 2
@echo "Starting MCP server (role: $(MCP_ROLE))..."
@OPENINTENT_SERVER_URL=http://localhost:$(PORT) \
OPENINTENT_API_KEY=dev-user-key \
OPENINTENT_MCP_ROLE=$(MCP_ROLE) \
$(NPX) -y @openintentai/mcp-server

check: ## Verify installation and connectivity
@echo "Checking Python SDK..."
@$(PYTHON) -c "import openintent; print(f' openintent {openintent.__version__}')" 2>/dev/null || echo " openintent: NOT INSTALLED"
@echo "Checking MCP SDK..."
@$(PYTHON) -c "import mcp; print(' mcp: OK')" 2>/dev/null || echo " mcp: NOT INSTALLED (run: make setup-mcp)"
@echo "Checking MCP server (Node)..."
@$(NPX) -y @openintentai/mcp-server --version 2>/dev/null && echo " mcp-server: OK" || echo " mcp-server: NOT INSTALLED (run: make setup-mcp)"
@echo "Checking OpenIntent server..."
@curl -sf http://localhost:$(PORT)/api/v1/intents > /dev/null 2>&1 && echo " server: RUNNING on port $(PORT)" || echo " server: NOT RUNNING (run: make server)"
@echo "Checking Python SDK..."
@$(PYTHON) -c "import openintent; print(f' openintent {openintent.__version__}')" 2>/dev/null || echo " openintent: NOT INSTALLED"
@echo "Checking MCP SDK..."
@$(PYTHON) -c "import mcp; print(' mcp: OK')" 2>/dev/null || echo " mcp: NOT INSTALLED (run: make setup-mcp)"
@echo "Checking MCP server (Node)..."
@$(NPX) -y @openintentai/mcp-server --version 2>/dev/null && echo " mcp-server: OK" || echo " mcp-server: NOT INSTALLED (run: make setup-mcp)"
@echo "Checking OpenIntent server..."
@curl -sf http://localhost:$(PORT)/api/v1/intents > /dev/null 2>&1 && echo " server: RUNNING on port $(PORT)" || echo " server: NOT RUNNING (run: make server)"

clean: ## Remove build artifacts and caches
rm -rf build/ dist/ *.egg-info .pytest_cache .mypy_cache .ruff_cache
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
rm -rf build/ dist/ *.egg-info .pytest_cache .mypy_cache .ruff_cache
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ pip install -e ".[dev,server]"

pytest # Run tests
ruff check openintent/ # Lint
black openintent/ # Format
ruff format openintent/ # Format
mypy openintent/ # Type check
openintent-server # Start dev server
```
Expand Down
30 changes: 30 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ All notable changes to the OpenIntent SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.14.1] - 2026-02-27

### Fixed

- **SDK/Server field format mismatches** — Fixed three category of mismatches between the Python SDK client and the protocol server's Pydantic models:
- **`constraints` type mismatch** — `create_intent()`, `create_child_intent()`, and `IntentSpec` sent `constraints` as a `list[str]` (e.g., `["rule1", "rule2"]`), but the server expects `Dict[str, Any]` (e.g., `{"rules": [...]}`). All three methods (sync and async) now accept and send `dict[str, Any]`. `Intent.from_dict()` retains backward compatibility for legacy list-format constraints.
- **`createdBy` → `created_by` for portfolios** — `create_portfolio()` sent `createdBy` and `governancePolicy` (camelCase) but the server's `PortfolioCreate` model expects `created_by` and `governance_policy` (snake_case). Fixed in both sync and async clients.
- **`get_portfolio_intents()` response parsing** — The server returns a raw JSON array from `GET /api/v1/portfolios/{id}/intents`, but the SDK expected a `{"intents": [...]}` wrapper dict, silently returning an empty list.

- **Silent empty results from list endpoints** — Seven additional list-returning methods used `data.get("key", [])` which silently returned empty lists when the server sent raw JSON arrays. All now use `isinstance(data, list)` detection to handle both raw array and wrapped dict responses:
- `list_portfolios()` — expected `{"portfolios": [...]}`
- `get_intent_portfolios()` — expected `{"portfolios": [...]}`
- `get_attachments()` — expected `{"attachments": [...]}`
- `get_costs()` — expected `{"costs": [], "summary": {}}`
- `get_failures()` — expected `{"failures": [...]}`
- `get_subscriptions()` — expected `{"subscriptions": [...]}`
- `federation_list_agents()` — expected `{"agents": [...]}`

- **`IntentLease.from_dict()` KeyError on server responses** — `acquire_lease()` threw `KeyError('status')` because the server's `LeaseResponse` model does not include a `status` field (it uses `acquired_at`, `expires_at`, and `released_at` to represent lease state). `IntentLease.from_dict()` now derives status from these fields: `RELEASED` if `released_at` is set, `EXPIRED` if `expires_at` is in the past, otherwise `ACTIVE`. Also handles the field name difference `acquired_at` (server) vs `created_at` (SDK). Backward compatible with the SDK's own serialization format.

- **Stale database singleton after server restart** — `get_database()` cached the `Database` instance at module level and never checked whether `database_url` changed between calls. When the protocol server restarted on a different port (e.g., `openintent_server_8001.db` → `openintent_server_8002.db`), the singleton kept pointing at the old file. Writes went to the old database; reads came from the new (empty) one — intents appeared created but were invisible to `list_intents`. The singleton now tracks its URL and recreates the connection when the URL changes.

- **Example and test updates** — All examples (`basic_usage.py`, `openai_multi_agent.py`, `multi_agent/coordinator.py`, `compliance_review/coordinator.py`) and tests updated to use dict-format constraints.

### Changed

- All version references updated to 0.14.1 across Python SDK, MCP server package, and changelog.

---

## [0.14.0] - 2026-02-25

### Added
Expand Down
10 changes: 6 additions & 4 deletions examples/basic_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ def main():
intent = client.create_intent(
title="Example Research Task",
description="Demonstrate OpenIntent SDK capabilities",
constraints=[
"Must complete within reasonable time",
"Log all significant activities",
],
constraints={
"rules": [
"Must complete within reasonable time",
"Log all significant activities",
]
},
initial_state={
"phase": "initialization",
"progress": 0.0,
Expand Down
8 changes: 4 additions & 4 deletions examples/compliance_review/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ async def plan(
title="Document Extraction",
description=f"Extract text and structure from: {document_name}",
assign="ocr-agent",
constraints=["max_retries:3", "backoff:exponential"],
constraints={"rules": ["max_retries:3", "backoff:exponential"]},
initial_state={
"phase": "extraction",
"retry_policy": {
Expand All @@ -101,7 +101,7 @@ async def plan(
description="Analyze document clauses for compliance issues",
assign="analyzer-agent",
depends_on=["Document Extraction"],
constraints=["lease_per_section:true"],
constraints={"rules": ["lease_per_section:true"]},
initial_state={
"phase": "analysis",
"leasing_enabled": True,
Expand All @@ -113,7 +113,7 @@ async def plan(
description="Assess overall document risk and compliance score",
assign="risk-agent",
depends_on=["Clause Analysis"],
constraints=["track_costs:true", "budget_usd:1.00"],
constraints={"rules": ["track_costs:true", "budget_usd:1.00"]},
initial_state={
"phase": "risk",
"cost_tracking": True,
Expand All @@ -125,7 +125,7 @@ async def plan(
description="Generate comprehensive compliance report",
assign="report-agent",
depends_on=["Risk Assessment"],
constraints=["output_formats:json,markdown"],
constraints={"rules": ["output_formats:json,markdown"]},
initial_state={
"phase": "report",
"output_formats": ["json", "markdown"],
Expand Down
6 changes: 4 additions & 2 deletions examples/multi_agent/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ async def plan(self, topic: str) -> PortfolioSpec:
title="Research Phase",
description=f"Research the topic: {topic}",
assign="research-agent",
constraints=["max_cost_usd:0.50", "required_confidence:0.75"],
constraints={
"rules": ["max_cost_usd:0.50", "required_confidence:0.75"]
},
initial_state={"phase": "research"},
),
# Phase 2: Writing (depends on Research)
Expand All @@ -69,7 +71,7 @@ async def plan(self, topic: str) -> PortfolioSpec:
description=f"Write a blog post about: {topic}",
assign="writing-agent",
depends_on=["Research Phase"], # Waits for research
constraints=["max_cost_usd:1.00", "style:engaging"],
constraints={"rules": ["max_cost_usd:1.00", "style:engaging"]},
initial_state={"phase": "writing"},
),
],
Expand Down
12 changes: 7 additions & 5 deletions examples/openai_multi_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,13 @@ async def create_research_intent(self, topic: str) -> str:
intent = await self.client.create_intent(
title=f"Research: {topic}",
description=f"Conduct research and synthesize findings on: {topic}",
constraints=[
"Research must be completed before synthesis",
"Only one agent may work on each scope at a time",
"All activities must be logged to the event stream",
],
constraints={
"rules": [
"Research must be completed before synthesis",
"Only one agent may work on each scope at a time",
"All activities must be logged to the event stream",
]
},
initial_state={
"topic": topic,
"research_status": "pending",
Expand Down
Loading