Metadata-Version: 2.4
Name: formcoax
Version: 0.1.2
Summary: A developer-first open-core library for conversational data intake
Author: FormCoax Contributors
License: MIT
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Provides-Extra: google
Requires-Dist: langchain-google-genai>=1.0.0; extra == 'google'
Description-Content-Type: text/markdown

# FormCoax 🚀

**FormCoax** is a developer-first, open-core python library for conversational data intake. It replaces rigid, blocking forms with non-blocking, agent-driven conversational intake natively in SaaS and enterprise web products.

Instead of a consumer-facing landing page builder, FormCoax is a **headless backend SDK** that manages form states and agent-coaxing logic.

📖 **Learn More:** Check out our [Features & Architecture Guide](docs/features.md) for a deep-dive on dynamic turn-scaling, context-aware extraction, evasion state machines, and detailed validation projections.

---

## 1. Why FormCoax? The Anti-Pattern we Solve

Naively sending a chat history to an LLM and asking it to "figure out what's missing from the form" leads to **context drift, high latency, hallucinated form fields, and a terrible user experience**.

FormCoax enforces a **deterministic State-Machine Driven Multi-Agent Pattern**:
1. **The State is the Single Source of Truth:** Your form fields are tracked structurally with confidence scores, validation flags, descriptions, and depth requirements.
2. **Context-Aware Evaluator Agent (Fast, Cheap):** Rather than evaluating user messages in isolation, the Evaluator analyzes the complete conversation history (`state.chat_history`). It uses **Structured Outputs** to extract values, run quality validation, flag shallow/vague details (e.g. *"I want it to be exciting"*), and programmatically identify explicit user skips/evasions.
3. **Adaptive Interviewer Agent (Empathetic, Conversational):** It examines the missing or shallow checklist, utilizing precise field descriptions, validation error logs, and current values to compose context-aware follow-ups or reprompts. It also supports customizable initial greetings.
4. **Intelligent Stagnation & Scaling Guardrails:** Rather than a rigid hardcoded turn cap, the conversation turn limit scales dynamically based on form complexity (e.g., 3 turns per required field). Furthermore, if a user repeatedly evades or refuses questions, the system detects this stagnation and triggers an early soft-exit—submitting the partial form state gracefully.

---

## 2. Recommended 2026 Model Selection

For optimal performance, leverage the current 2026 flagship frontier models:

| Role | Recommended Model | Why? |
| --- | --- | --- |
| **Evaluator (Extraction)** | **Gemini 3.5 Flash** | Low latency, sub-second response times, and near-perfect structured tool calling for a fraction of the cost. |
| **Interviewer (Coaxer)** | **Gemini 3.1 Pro** or **Claude 3.5 Sonnet** | Premium conversational nuance, high empathy, and great context retention. |

---

## 3. Installation

Install the stable release directly from PyPI:

```bash
pip install formcoax
```

For developers working locally or contributing to the codebase, install in editable mode with development dependencies:

```bash
pip install -e ".[dev]"
```

> [!IMPORTANT]
> **LangChain Interface Dependency:**
> FormCoax engines (`EvaluatorEngine` and `InterviewerEngine`) do not call model APIs directly. Instead, they expect **LangChain-compatible ChatModel instances** (such as `ChatGoogleGenerativeAI`, `ChatOpenAI`, or `ChatAnthropic`).
> - The **Evaluator** requires a model supporting structured outputs via `.with_structured_output(...)`.
> - The **Interviewer** requires a model supporting async generation via `.ainvoke(...)`.

---

## 4. Key Components & How to Use Them

### A. Define your Target Form Schema
You define your intake goals using a standard `Pydantic v2` model. Use `json_schema_extra` parameter keys to configure **validation depth**:

```python
from pydantic import BaseModel, Field
from typing import Optional

class StartupOnboardingForm(BaseModel):
    company_name: str = Field(..., description="The official company name.")
    project_budget: int = Field(..., description="Estimated software project budget in USD.")
    
    # Enforce depth validation: must be at least 10 words or it's flagged as 'shallow'
    problem_description: str = Field(
        ...,
        description="Detailed description of the problem you are solving.",
        json_schema_extra={"depth": "detailed", "min_words": 10}
    )
```

---

Generate the centralized `SessionState` directly from your Pydantic model. By omitting `max_turns`, FormCoax automatically scales the conversational turn ceiling to match your schema size (3 turns per required field):

```python
from formcoax import SessionState

state = SessionState.from_pydantic_model(
    session_id="user-session-101",
    model_class=StartupOnboardingForm,
    # max_turns is omitted here to enable dynamic scaling:
    # 3 required fields * 3 = 9 turns (with safe floor of 5, ceiling of 15)
)

print(state.max_turns)
# Output: 9

print(state.max_evasions)
# Output: 3 (Consecutive evasion limit before early soft-exit)

print(state.missing_or_shallow_fields)
# Output: ['company_name', 'project_budget', 'problem_description']
```

---

### C. State Persistence (State Stores)
FormCoax provides an abstract `BaseStateStore` for integrating your own database (PostgreSQL, Redis, MongoDB, etc.), alongside a default thread-safe, copying `InMemoryStateStore` for testing and development:

```python
from formcoax import InMemoryStateStore

store = InMemoryStateStore()

# Save state
await store.save_state(state.session_id, state)

# Retrieve state (isolated deep-copy to prevent side-effects)
state_copy = await store.get_state(state.session_id)
```

To create a production database store, subclass `BaseStateStore`:

```python
from formcoax import BaseStateStore, SessionState
from typing import Optional

class PostgreSQLStateStore(BaseStateStore):
    async def get_state(self, session_id: str) -> Optional[SessionState]:
        # Fetch from PostgreSQL, deserialize JSON into SessionState
        ...
        
    async def save_state(self, session_id: str, state: SessionState) -> None:
        # Serialize SessionState to JSON and upsert in PostgreSQL
        ...

    async def delete_state(self, session_id: str) -> None:
        # Delete record
        ...
```

---

The `EvaluatorEngine` hooks up to an LLM supporting structured tool-calling. It extracts values, runs validation, detects shallow details, and flags evasions while analyzing the complete conversation history:

```python
from langchain_google_genai import ChatGoogleGenerativeAI
from formcoax import EvaluatorEngine

# Configure Gemini 3.5 Flash (the 2026 industry-standard for fast structured calls)
llm = ChatGoogleGenerativeAI(model="gemini-3.5-flash", temperature=0.0)
evaluator = EvaluatorEngine(llm=llm, model_class=StartupOnboardingForm)

# User says: "We're Acme Corp. We need a fast backend built."
# Let's run context-aware extraction:
state = await evaluator.extract_and_hydrate(state, "We're Acme Corp. We need a fast backend built.")

# Results:
print(state.form_schema["company_name"].value)        # "Acme Corp"
print(state.form_schema["company_name"].shallow)      # False (No depth constraint)

print(state.form_schema["problem_description"].value) # "We need a fast backend built."
print(state.form_schema["problem_description"].shallow) # True (Vague details flag! Under min_words=10 quality gate)

# The checklist is updated. Since problem_description is too shallow, it remains on the list:
print(state.missing_or_shallow_fields)
# Output: ['project_budget', 'problem_description']

# If the user says: "I don't know, skip this for now"
# The Evaluator detects the evasion, updating consecutive_evasions:
state = await evaluator.extract_and_hydrate(state, "I don't know, skip this for now")
print(state.consecutive_evasions) # Output: 1
```

---

The `InterviewerEngine` examines missing fields and generates empathetic questions. When a field is shallow, the Interviewer receives detailed status metadata (its current value and validation error) to frame highly specific reprompting follow-ups. It also supports customizable initial greetings:

```python
from langchain_google_genai import ChatGoogleGenerativeAI
from formcoax import InterviewerEngine, ChatMessage

# Configure Gemini 3.1 Pro (the 2026 reasoning model)
chat_llm = ChatGoogleGenerativeAI(model="gemini-3.1-pro-preview", temperature=0.7)
interviewer = InterviewerEngine(
    llm=chat_llm,
    initial_greeting="Let's create a project. What do you want to accomplish?"
)

# Print initial opening line:
print(interviewer.initial_greeting)
# Output: "Let's create a project. What do you want to accomplish?"

# If we are in the middle of a session, it coaxes the next missing/shallow field,
# acknowledging any shallow details instead of starting from scratch:
reply = await interviewer.generate_response(state)
print(reply)
# Output: "That's a neat starting point! Could you expand on what the backend should do so we can capture the specific details?"

# Append to history and record a turn
state.chat_history.append(ChatMessage(role="assistant", content=reply))
state.turn_count += 1
```

---

## 5. Standard End-to-End Orchestration Loop

This is how you integrate FormCoax inside your backend controller, API endpoint (FastAPI), or state machine (LangGraph):

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from formcoax import InMemoryStateStore, SessionState, ChatMessage, EvaluatorEngine, InterviewerEngine
from langchain_google_genai import ChatGoogleGenerativeAI

app = FastAPI()
store = InMemoryStateStore()

# Define your intake form target
class IntakeForm(BaseModel):
    user_name: str
    project_budget: int

# Initialize Agents
eval_llm = ChatGoogleGenerativeAI(model="gemini-3.5-flash", temperature=0.0)
chat_llm = ChatGoogleGenerativeAI(model="gemini-3.1-pro-preview", temperature=0.7)

evaluator = EvaluatorEngine(llm=eval_llm, model_class=IntakeForm)
interviewer = InterviewerEngine(llm=chat_llm)

class ChatRequest(BaseModel):
    session_id: str
    message: str

@app.post("/chat")
async def chat_turn(req: ChatRequest):
    # 1. Retrieve the existing state, or initialize a new one if it's a new session
    state = await store.get_state(req.session_id)
    if not state:
        # max_turns is omitted here to enable dynamic turn-scaling:
        # 2 required fields * 3 = 6 turns (with safe floor of 5, ceiling of 15)
        state = SessionState.from_pydantic_model(req.session_id, IntakeForm)

    # 2. Add user message and advance the turn counter
    state.chat_history.append(ChatMessage(role="user", content=req.message))
    state.turn_count += 1

    # 3. RUN EVALUATOR: Extract details and validate depth
    state = await evaluator.extract_and_hydrate(state, req.message)

    # 4. RUN INTERVIEWER: Generate empathetic question or soft-exit closeout
    reply = await interviewer.generate_response(state)
    state.chat_history.append(ChatMessage(role="assistant", content=reply))

    # 5. Persist state
    await store.save_state(req.session_id, state)

    return {
        "reply": reply,
        "is_complete": state.is_complete,
        "is_submitted": state.is_complete or state.is_turn_limit_reached,
        "extracted_data": {k: v.value for k, v in state.form_schema.items()}
    }
```

---

## 6. Running Tests

FormCoax ships with robust, offline unit tests using mocks:

```bash
pytest
```

---

## 7. Contributing & Community

FormCoax is open source and we welcome contributions of all types! Whether you are a first-time contributor or an experienced open-source developer:
* Check out our friendly [Contribution Guide](CONTRIBUTING.md) to set up your local development environment and submit your first pull request.
* Help us expand the SDK by adding nested schemas, branching form support, or new state persistence backends!

