Any framework¶
Two framework-neutral helpers in thinkless.integrations cover the rest:
CrewAI, LlamaIndex, Pydantic AI, AutoGen, a hand-written tool loop, or a web
service. Both are in the core package.
Route on a decision¶
route decides a question and returns the destination for an accepted
answer, or the default for anything else. Destinations can be anything: a
function, an agent object, a queue name.
from thinkless.integrations import route
handler = route(
engine,
ticket_text,
INTENT,
{"order_status": answer_from_tracking, "refund": refund_flow},
default=llm_agent.run, # what you call today
)
reply = handler(ticket_text)
Router is the reusable form, with async support:
from thinkless.integrations import Router
intent = Router(engine, INTENT, {"order_status": tracking_crew}, default=general_crew)
crew = intent(ticket_text) # or: await intent.aroute(ticket_text)
state= turns whatever the router is called with into the question's input,
and last_user_text reads the latest user message out of LangChain
messages, OpenAI chat messages and Responses API input items:
from thinkless.integrations import Router, last_user_text
intent = Router(engine, INTENT, routes, default, state=last_user_text)
intent(messages)
Gate a tool call¶
gate checks a call before it runs. It wraps the function, keeps its name,
docstring and signature (so frameworks that build tool schemas from the
signature see no change), and works on plain and async functions:
from thinkless import Engine, YesNo
from thinkless.integrations import gate
from thinkless.providers import Rules
REFUND_OK = YesNo("Is this refund within policy?", name="refund_ok")
policy = Rules()
@policy.rule("refund_ok")
def within_policy(state):
return state["arguments"]["amount"] <= 100
@gate(Engine([policy]), REFUND_OK, on_block=lambda decision: "Refunds over $100 need a person.")
def refund(order_id: str, amount: float) -> str:
"""Refund an amount on an order."""
...
By default the question is asked about
{"tool": "refund", "arguments": {"order_id": ..., "amount": ...}}. The call
runs only when the decision is accepted and equals allow_when (default
True). on_uncertain="allow" lets uncertain calls through. Without
on_block, a blocked call raises ToolBlockedError, whose decision
attribute says which provider answered and how.
Put gate under the framework's own tool decorator. With Pydantic AI:
@agent.tool_plain
@gate(engine, REFUND_OK, on_block=lambda d: "Refunds over $100 need a person.")
def refund(order_id: str, amount: float) -> str:
"""Refund an amount on an order."""
...
With LangChain and LangGraph, under @tool, as in the
LangGraph guide. Both are tested: the generated
tool schema is unchanged. Other decorators that build the schema from the
function signature, such as CrewAI's @tool or LlamaIndex's
FunctionTool.from_defaults, follow the same pattern; check the generated
schema once with your version.
In a hand-written loop¶
When you own the loop, call the engine directly where the decisions are:
decisions = engine.decide_many(message, [INTENT, WANTS_HUMAN, ORDER])
if decisions["wants_human"].is_(True):
return hand_to_person(message)
if decisions["intent"].is_("order_status") and decisions["order"].accepted:
return tracking_reply(decisions["order"].value["order_id"])
return run_llm_agent(message) # the path you have today
is_() is false for uncertain decisions, so every branch that acts on a
decision is taken only when a provider was confident. Everything else falls
through to the existing code.