Skip to main content

A developer-first open-core library for conversational data intake

Project description

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 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:

pip install formcoax

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

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:

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):

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:

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:

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:

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:

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):

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:

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 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!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

formcoax-0.1.2.tar.gz (29.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

formcoax-0.1.2-py3-none-any.whl (16.1 kB view details)

Uploaded Python 3

File details

Details for the file formcoax-0.1.2.tar.gz.

File metadata

  • Download URL: formcoax-0.1.2.tar.gz
  • Upload date:
  • Size: 29.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for formcoax-0.1.2.tar.gz
Algorithm Hash digest
SHA256 457cf03cf3fa8194154de4ca92ceb27953e90259c0ab1777ce2de846daca2501
MD5 1ec70d3453b80836a52d29c178460f88
BLAKE2b-256 214a15a60f7ac975f718db3e1c5f9840a28cf7f65a72df618b3da59894acd248

See more details on using hashes here.

Provenance

The following attestation bundles were made for formcoax-0.1.2.tar.gz:

Publisher: publish.yml on formcoax/formcoax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file formcoax-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: formcoax-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 16.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for formcoax-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 51640399003bc9e0d9d393cf7b06970b6831de135fb0361f1c5225dad03a2958
MD5 2b2a04b119aaf2156ab3b52fb82c52ec
BLAKE2b-256 e3929b8e7e2170fcb51f391a3e242301be56fad89ba9fc9200332e241ca64177

See more details on using hashes here.

Provenance

The following attestation bundles were made for formcoax-0.1.2-py3-none-any.whl:

Publisher: publish.yml on formcoax/formcoax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page