Metadata-Version: 2.4
Name: wayfinder-agents
Version: 0.1.16
Summary: LangGraph-orchestrated AI agent framework for multi-domain entity management
Project-URL: Homepage, https://github.com/jspenc72/wayfinder-agents
Project-URL: Repository, https://github.com/jspenc72/wayfinder-agents
Project-URL: Issues, https://github.com/jspenc72/wayfinder-agents/issues
Project-URL: Changelog, https://github.com/jspenc72/wayfinder-agents/blob/main/CHANGELOG.md
Author: Jesse Spencer
License: Wayfinder Community License 1.0
License-File: LICENSE
Keywords: ai-agents,fastapi,grok,langchain,langgraph,llm,multi-agent,openai,workflow
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: langchain-openai>=0.2
Requires-Dist: langchain>=0.3
Requires-Dist: langgraph>=0.2
Requires-Dist: langsmith>=0.1
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: pydantic>=2.9
Requires-Dist: structlog>=24.4
Requires-Dist: tenacity>=9.0
Requires-Dist: uvicorn[standard]>=0.30
Provides-Extra: all
Requires-Dist: geopy>=2.4; extra == 'all'
Requires-Dist: langgraph-checkpoint-mongodb>=0.1; extra == 'all'
Requires-Dist: langgraph-checkpoint-postgres>=2.0; extra == 'all'
Requires-Dist: langgraph-checkpoint-redis>=0.0.5; extra == 'all'
Requires-Dist: langgraph-checkpoint<4.1.1,>=4.1.0; extra == 'all'
Requires-Dist: motor>=3.6; extra == 'all'
Requires-Dist: pymongo>=4.9; extra == 'all'
Requires-Dist: redis>=5.2; extra == 'all'
Requires-Dist: wikipedia-api>=0.7; extra == 'all'
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Provides-Extra: mongodb
Requires-Dist: langgraph-checkpoint-mongodb>=0.1; extra == 'mongodb'
Requires-Dist: motor>=3.6; extra == 'mongodb'
Requires-Dist: pymongo>=4.9; extra == 'mongodb'
Provides-Extra: postgres
Requires-Dist: langgraph-checkpoint-postgres>=2.0; extra == 'postgres'
Provides-Extra: redis
Requires-Dist: langgraph-checkpoint-redis>=0.0.5; extra == 'redis'
Requires-Dist: langgraph-checkpoint<4.1.1,>=4.1.0; extra == 'redis'
Requires-Dist: redis>=5.2; extra == 'redis'
Provides-Extra: research
Requires-Dist: geopy>=2.4; extra == 'research'
Requires-Dist: wikipedia-api>=0.7; extra == 'research'
Description-Content-Type: text/markdown

# wayfinder-agents

> **LangGraph-orchestrated AI agent framework for multi-domain entity management.**

`wayfinder-agents` is a generic, production-grade Python framework for building LLM-powered workflows that create, modify, validate, and publish any structured entity type via a REST API. Built on LangGraph and LangChain.

---

## Quick Start

```bash
pip install wayfinder-agents
```

```python
from wayfinder import WayfinderApp
from wayfinder.decorators import domain, Field

@domain("product", api="/api/products")
class Product:
    """An e-commerce product."""
    name: str = Field(required=True, description="Product name")
    description: str = Field(required=True, description="Full description")
    price_cents: int = Field(type="int", required=True, description="Price in cents")
    category: str = Field(enum=["electronics", "clothing", "books"])

app = WayfinderApp(
    name="ShopBot",
    api_base_url="http://localhost:3000",
)
app.discover_domains("myapp.domains")
app.serve(port=8001)
```

---

## What It Does

The framework provides a 16-node LangGraph workflow out of the box:

| Node | Role |
|------|------|
| `context_resolver` | Detects topic changes between conversation turns |
| `coordinator` | Routes requests to the right specialist (LLM-based + deterministic fast-paths) |
| `confirm` | Human-in-the-loop approval gate before write actions |
| `plan` | Decomposes multi-step requests into sub-tasks |
| `research` | Fetches external data (Wikipedia, geocoding, images) |
| `compose` | Creates a new entity draft from research notes |
| `modify` | Edits existing entities |
| `review` | Quality validation before publishing |
| `publish` | Saves via REST API |
| `propose` | Suggests changes without immediately modifying |
| `remediate` | Fixes API validation errors and retries |
| `delete` / `duplicate` | Entity lifecycle operations |
| `query` | Read-only Q&A about existing entities |
| `summarize` | Formats the final response |

Each node can be overridden per-domain.

---

## Domain Registration

### Using the `@domain` decorator

```python
from wayfinder.decorators import domain, subdoc, Field, Subdoc, Rel

@subdoc
class Address:
    street: str = Field(required=True)
    city: str = Field(required=True)

@domain("venue", api="/api/venues", progressive_save="rooms:5")
class Venue:
    """A physical event venue."""
    name: str = Field(required=True)
    address: Address = Subdoc(Address)
    capacity: int = Field(type="int")
    events: object = Rel("event", has_many=True, foreign_key="venue_id")
```

### LLM de-identification (field aliasing & redaction)

For sensitive data (for example HIPAA/PHI fields like patient names or SSNs),
you can mark specific fields so Wayfinder masks them before sending prompts to
the LLM and restores the originals from structured JSON responses.

Two complementary mechanisms are available per-field:

- **Aliasing** (`llm_alias=True`) — replaces values with deterministic
    referenceable tokens (`PATIENT_1`, `CLINICIAN_2`). The LLM can still
    reason about and reference the entity in its output.
- **Redaction** (`llm_redact=True`) — replaces values with an opaque
    placeholder (`[REDACTED]` by default, configurable via
    `llm_redact_value`). The LLM cannot meaningfully reference the value;
    originals are restored from a positional store after the LLM call.

```python
from wayfinder.decorators import domain, Field, subdoc, Subdoc

@subdoc
class Visit:
    clinician_name: str = Field(llm_alias=True, llm_alias_prefix="clinician")
    note: str = Field()

@domain("patient_record", api="/api/patient-records")
class PatientRecord:
    patient_name: str = Field(required=True, llm_alias=True, llm_alias_prefix="patient")
    ssn: str = Field(llm_redact=True, llm_redact_value="***")
    diagnosis: str = Field(required=True)
    visits: list[Visit] = Subdoc(Visit)
```

For brevity, two classmethod shortcuts produce the same configuration:

```python
patient_name: str = Field.aliased("patient", required=True)
ssn: str = Field.redacted("***")
```

Behavior summary:

- **Aliased fields**: same value at the same field path reuses the same
    token; restored to original after the LLM returns JSON.
- **Redacted fields**: every value at that path is replaced with the
    placeholder; originals are stashed by position (including list index)
    and restored even if the LLM mutates or drops the field. Redaction
    takes precedence when both `llm_alias` and `llm_redact` are set.

Current scope:

- Built into draft-based LLM flows (`compose`, `modify`, `review`).
- Targets domain fields you explicitly mark with `llm_alias=True` or
    `llm_redact=True`.
- Does not automatically scrub arbitrary free-text conversation history; if
    you include PHI in user messages, redact upstream before sending.

### Using a Pydantic model

```python
from pydantic import BaseModel
from wayfinder.adapters import domain_from_model

class Article(BaseModel):
    """A blog article."""
    title: str
    body: str
    tags: list[str] = []

domain_from_model(Article, api="/api/articles")
```

---

## Domain Customisation

### Override a node

```python
from wayfinder.decorators import node

@node("modify", domain="product")
async def my_modifier(state):
    # custom modification logic
    return {"draft": {...}, "next_step": "review"}
```

### Lifecycle events

```python
from wayfinder.decorators import on

@on("before_publish", domain="product")
async def validate_images(state):
    if not state.draft.get("images"):
        state.draft["images"] = []
    return state
```

### Custom validator

```python
from wayfinder.domain import DomainValidator

class ProductValidator(DomainValidator):
    def validate(self, draft: dict, errors: list[str]) -> list[str]:
        if draft.get("price_cents", 0) <= 0:
            errors.append("price_cents must be greater than 0")
        return errors
```

---

## API Adapters

Out of the box the framework ships an adapter for **Express/Mongoose** APIs.
Implement `APIAdapter` for other backends:

```python
from wayfinder.api_adapter import APIAdapter, set_adapter

class DjangoRestAdapter(APIAdapter):
    def parse_response(self, json_body, entity_name): ...
    def parse_list_response(self, json_body, plural_name): ...
    def build_pagination_params(self, page, limit): ...
    async def discover_schema(self, client, api_base_url, model_name): ...

set_adapter(DjangoRestAdapter())
```

---

## Configuration

All settings use the `WAYFINDER_` env prefix with `__` as the nested delimiter:

```bash
WAYFINDER__LLM__DEFAULT_PROVIDER=openai
WAYFINDER__LLM__OPENAI_MODEL=gpt-4.1
WAYFINDER__API__BASE_URL=http://myapi:3000/api
WAYFINDER__API__SERVICE_TOKEN=my-internal-token
WAYFINDER__CHECKPOINT__BACKEND=redis
WAYFINDER__CHECKPOINT__REDIS_URL=redis://redis:6379/0
WAYFINDER__OBSERVABILITY__LANGSMITH_ENABLED=true
WAYFINDER__OBSERVABILITY__LANGSMITH_API_KEY=ls-...
```

---

## Checkpointing Backends

```bash
# PostgreSQL
pip install "wayfinder-agents[postgres]"
WAYFINDER__CHECKPOINT__BACKEND=postgres
WAYFINDER__CHECKPOINT__CONNECTION_STRING=postgresql+asyncpg://user:pass@host/db

# Redis
pip install "wayfinder-agents[redis]"
WAYFINDER__CHECKPOINT__BACKEND=redis
WAYFINDER__CHECKPOINT__REDIS_URL=redis://localhost:6379/0

# MongoDB
pip install "wayfinder-agents[mongodb]"
WAYFINDER__CHECKPOINT__BACKEND=mongodb
WAYFINDER__CHECKPOINT__CONNECTION_STRING=mongodb://localhost:27017
```

---

## Publishing to PyPI

```bash
# Build
python -m build

# Test on TestPyPI first
twine upload --repository testpypi dist/*

# Publish
twine upload dist/*
```

The GitHub Actions workflow at `.github/workflows/publish.yml` automates this on tagged releases.

---

## License

`wayfinder-agents` is available under the `Wayfinder Community License 1.0`.

Free use is permitted for:

- individuals using the project for personal, educational, research, or
    non-commercial work;
- nonprofits and educational institutions for non-commercial internal use; and
- organizations with less than USD 100,000 in annual gross revenue.

A separate commercial license is required for:

- organizations at or above USD 100,000 in annual gross revenue;
- paid products, paid client work, and internal commercial production use; and
- hosted or managed-service offerings based on the software.

See [LICENSE](LICENSE), [licensing strategy](docs/licensing/strategy.md), and
[commercial terms summary](docs/licensing/COMMERCIAL-TERMS-DRAFT.md).
