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:
- The State is the Single Source of Truth: Your form fields are tracked structurally with confidence scores, validation flags, descriptions, and depth requirements.
- 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. - 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.
- 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 (
EvaluatorEngineandInterviewerEngine) do not call model APIs directly. Instead, they expect LangChain-compatible ChatModel instances (such asChatGoogleGenerativeAI,ChatOpenAI, orChatAnthropic).
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file formcoax-0.1.0.tar.gz.
File metadata
- Download URL: formcoax-0.1.0.tar.gz
- Upload date:
- Size: 28.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc8f4417b34ee2b7de6f3c32c10a581c7d42fbd42fdd5d64bda0645b908335db
|
|
| MD5 |
7a341eed3c24bb82c8f1681fe9ad8307
|
|
| BLAKE2b-256 |
61b97ff161f7ed4bcffc4844be4c3ca965c3003e22abd4482d810ba54bcd9edb
|
File details
Details for the file formcoax-0.1.0-py3-none-any.whl.
File metadata
- Download URL: formcoax-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
caafbba08e03473757290d80c7ce4e6c551b60d9a3809298e4e1d76efd0de631
|
|
| MD5 |
4baebd61a2488d47894c3be4e7a7946e
|
|
| BLAKE2b-256 |
9c8516ea629b0dc4fcf28ee2d31444820b37fd1788d3ef2731a8c2b4eb3c2051
|