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 package along with its dependencies:

pip install -e .

To install development dependencies (for pytest):

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.1.tar.gz (28.9 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.1-py3-none-any.whl (16.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: formcoax-0.1.1.tar.gz
  • Upload date:
  • Size: 28.9 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.1.tar.gz
Algorithm Hash digest
SHA256 ff5f455764662319e37fc8972ce652f2cb1449bf618fb7c433e55b974b7cf4f1
MD5 03d9e807513391962a511eb80307f0a9
BLAKE2b-256 229dbffba13b4fb0ea430f18d1a0ae9b73e5cdb04a64fcd6d491fde2024bdca4

See more details on using hashes here.

Provenance

The following attestation bundles were made for formcoax-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: formcoax-0.1.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e86baa5f58971860a8302c0f4c2f8c2c4b6d7b6a2bd6074d8acfc27c1fe600cb
MD5 4027fcd5eb6b433278d083a3ff4dff29
BLAKE2b-256 9b5ed21205c1d992938c0a47fc88060ff2d5ce56730d664cfe5f698dd4ef4572

See more details on using hashes here.

Provenance

The following attestation bundles were made for formcoax-0.1.1-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