tati-langchain
A framework-agnostic AI/LangChain engine you can pip install into any
Python project (Django, FastAPI, Flask, a script, …).
Covers the pieces every AI app ends up re-building:
| Capability | Entry point |
|---|---|
| OpenAI / Anthropic / Bedrock providers | Provider, ModelSpec, ProviderStack, build_chat_model |
| Text generation | generate_text |
| Image generation | built-in generate_image tool (OpenAI Images API) |
| Long-form writing / research | high max_output_tokens + native web search |
| Cost extraction per run | extract_usage, calculate_message_cost, cost_for_agent_result |
| Structured outputs (Pydantic) | generate_structured, with_structured_output |
| Agentic tool loop | run_tool_loop |
| Custom tools | define_tool / @tool, bind_extra_tools |
Every Django/ORM/settings dependency from the source project has been swapped for plain dataclasses and explicit function arguments.
Install
Published on PyPI.
pip install tati-langchain
# + AWS Bedrock support
pip install "tati-langchain[bedrock]"
From a consuming project (requirements.in)
tati-langchain>=0.3.1
# or with Bedrock:
# tati-langchain[bedrock]>=0.3.1
Then:
pip install -r requirements.in
Set credentials the normal LangChain way:
- OpenAI →
OPENAI_API_KEY - Anthropic →
ANTHROPIC_API_KEY - Bedrock → standard AWS credentials (
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION, or an instance role)
1. Pick a provider and build a model
from decimal import Decimal
from tati_langchain import ModelSpec, ProviderStack, Provider, build_chat_model
# --- OpenAI ---
openai_stack = ProviderStack(
provider=Provider.OPENAI,
display_name="OpenAI",
chat_model=ModelSpec(
provider=Provider.OPENAI,
name="gpt-5.4-mini",
supports_vision=True,
supports_tools=True,
max_output_tokens=4096,
input_cost_per_1m_tokens=Decimal("0.15"),
output_cost_per_1m_tokens=Decimal("0.60"),
),
supports_web_search=True,
)
openai_bundle = build_chat_model(openai_stack)
# --- Anthropic ---
anthropic_stack = ProviderStack(
provider=Provider.ANTHROPIC,
display_name="Anthropic",
chat_model=ModelSpec(
provider=Provider.ANTHROPIC,
name="claude-haiku-4-5",
max_output_tokens=4096,
input_cost_per_1m_tokens=Decimal("0.80"),
output_cost_per_1m_tokens=Decimal("4.00"),
),
supports_web_search=True,
)
anthropic_bundle = build_chat_model(anthropic_stack)
# --- AWS Bedrock (Converse API — best for tool calling) ---
# requires: pip install "tati-langchain[bedrock]"
bedrock_stack = ProviderStack(
provider=Provider.BEDROCK_CONVERSE,
display_name="Bedrock",
chat_model=ModelSpec(
provider=Provider.BEDROCK_CONVERSE,
name="anthropic.claude-3-5-sonnet-20241022-v2:0",
max_output_tokens=4096,
extra_params={"region_name": "eu-west-1"}, # forwarded to ChatBedrockConverse
),
supports_web_search=False, # no native Bedrock web-search tool in this package
)
bedrock_bundle = build_chat_model(bedrock_stack, include_default_tools=False)
build_chat_model returns a ChatModelBundle:
bundle.chat_llm— model with tools bound (use withrun_tool_loop)bundle.raw_llm— unbound model (use withgenerate_text/generate_structured)bundle.tools_by_name— local tools the agent loop can execute
2. Text generation
from langchain_core.messages import HumanMessage, SystemMessage
from tati_langchain import generate_text
result = generate_text(
openai_bundle.raw_llm,
[
SystemMessage("You are a concise assistant."),
HumanMessage("Explain vector databases in two sentences."),
],
model=openai_stack.chat_model, # optional — enables result.cost
)
print(result.text)
print(result.usage) # {"input_tokens", "output_tokens", "cached_input_tokens"}
print(result.cost.total_cost if result.cost else None)
3. Long-form writing & research
Long-form = high max_output_tokens. Research = turn on native web search
(OpenAI / Anthropic) and ask the model to cite sources.
from langchain_core.messages import HumanMessage, SystemMessage
from tati_langchain import ModelSpec, Provider, ProviderStack, build_chat_model, run_tool_loop
research_stack = ProviderStack(
provider=Provider.OPENAI,
display_name="Research",
chat_model=ModelSpec(
provider=Provider.OPENAI,
name="gpt-5.4",
max_output_tokens=16000, # long-form headroom
),
supports_web_search=True, # binds the provider-native web_search tool
)
bundle = build_chat_model(research_stack)
messages = [
SystemMessage(
"You are a research analyst. Use web search. Write a structured brief "
"with a summary, key findings, and cited sources."
),
HumanMessage("What changed in EU AI Act enforcement in the last 6 months?"),
]
result = run_tool_loop(bundle.chat_llm, messages, bundle.tools_by_name)
print(result.ai_message.content)
4. Image generation
Built-in generate_image tool (OpenAI Images API). Works even on an Anthropic
/ Bedrock chat stack if you point image_model at an OpenAI image model.
from decimal import Decimal
from langchain_core.messages import HumanMessage
from tati_langchain import (
ImageModelSpec, ModelSpec, Provider, ProviderStack,
build_chat_model, run_tool_loop, calculate_image_cost,
)
stack = ProviderStack(
provider=Provider.OPENAI,
display_name="Creative",
chat_model=ModelSpec(provider=Provider.OPENAI, name="gpt-5.4-mini"),
image_model=ImageModelSpec(
provider=Provider.OPENAI,
name="gpt-image-1-mini",
text_input_cost_per_1m=Decimal("5.00"),
image_output_cost_per_1m=Decimal("40.00"),
),
)
bundle = build_chat_model(stack)
result = run_tool_loop(
bundle.chat_llm,
[HumanMessage("Draw a red fox wearing sunglasses")],
bundle.tools_by_name,
on_progress=print, # optional: "🎨 Image generation triggered..."
)
for att in result.attachments:
open("fox.png", "wb").write(att.data)
for usage in result.image_usages:
print(calculate_image_cost(model=stack.image_model, **usage["tokens"]))
5. Cost extraction (what a run actually cost)
from tati_langchain import extract_usage, calculate_message_cost, cost_for_agent_result
# Plain text turn
usage = extract_usage(result.ai_message)
breakdown = calculate_message_cost(model=stack.chat_model, **usage)
print(breakdown.total_cost, breakdown.currency)
# Full agent turn (chat tokens + any image tool usages)
message_cost, image_costs = cost_for_agent_result(
ai_message=result.ai_message,
model=stack.chat_model,
image_usages=result.image_usages,
)
print(message_cost.total_cost, [c.total_cost for c in image_costs])
Nothing is persisted — you decide whether that becomes a DB row, a log line, or a metrics counter.
6. Structured outputs with Pydantic
from pydantic import BaseModel, Field
from langchain_core.messages import HumanMessage
from tati_langchain import generate_structured, calculate_message_cost
class BookRec(BaseModel):
title: str
author: str
reason: str = Field(description="One-sentence why this fits")
structured = generate_structured(
openai_bundle.raw_llm,
[HumanMessage("Recommend one sci-fi book for a beginner.")],
BookRec,
)
print(structured.parsed.title, structured.parsed.author)
print(structured.usage)
if structured.raw_message is not None:
print(calculate_message_cost(model=openai_stack.chat_model, **structured.usage))
Or bind once and reuse:
from tati_langchain import with_structured_output
llm = with_structured_output(openai_bundle.raw_llm, BookRec)
rec = llm.invoke([HumanMessage("Recommend a mystery novel.")])
7. Agentic design + custom tools
from langchain_core.messages import HumanMessage
from tati_langchain import define_tool, build_chat_model, run_tool_loop, bind_extra_tools
@define_tool
def lookup_order(order_id: str) -> str:
"""Look up an order by id and return its status."""
return f"Order {order_id}: shipped"
# Option A — pass extra tools at build time
bundle = build_chat_model(openai_stack, extra_tools=[lookup_order])
# Option B — rebind onto an existing bundle
bundle = bind_extra_tools(bundle, [lookup_order])
result = run_tool_loop(
bundle.chat_llm,
[HumanMessage("Where is order A-100?")],
bundle.tools_by_name,
)
print(result.ai_message.content)
run_tool_loop is provider-agnostic: it invokes the model, executes any
local tool calls registered in tools_by_name, feeds results back, and
stops after a small iteration cap (or when a tool signals forced_reply /
limit_reached). Provider-native tools (e.g. web search) never appear in
tool_calls — the provider resolves them server-side.
Tool with an explicit Pydantic args schema
from pydantic import BaseModel, Field
from tati_langchain import define_tool
class SearchArgs(BaseModel):
query: str
limit: int = Field(default=5, ge=1, le=20)
@define_tool(args_schema=SearchArgs)
def search_docs(query: str, limit: int = 5) -> str:
"""Search the internal docs corpus."""
return f"top {limit} hits for {query!r}"
8. Document generation
generate_document degrades gracefully (returns an "unavailable" message to
the model, doesn't raise) until the optional tati-docgen package is also
installed — at which point it starts working with no code change.
Design principles
- You own persistence, config, and "what's active." This package never reads a global settings object and never writes to a database.
- Pure cost math, no side effects. Cost helpers return dataclasses; you decide how to store them.
- No messaging dependency. Tool attachments come back as this package's
own
ToolAttachment— map to WhatsApp/email/etc. at the call site.
What's not in this package (by design)
- Model-catalog / "which stack is active" storage
- Conversation history storage
- Free-trial / usage-limit gating (tools may still signal
limit_reached) - Sending replies to WhatsApp/email (see
tati-whatsapp) - i18n for progress strings — override via
progress_text_builders=
License
Proprietary — internal use only within Tati Software Pty Ltd.
See LICENSE.
Metadata
Release files for tati-langchain 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tati_langchain-0.3.2.tar.gz | 29.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tati_langchain-0.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.2 kB
Release files / tati_langchain-0.3.2.tar.gz
| Download URL | tati_langchain-0.3.2.tar.gz |
|---|---|
| Size | 29.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ab88922f0238e607d0c047a0b0b6def1f82e990525d55cb474525b819bd7ab74
|
|
BLAKE2b-256 checksum How to use checksums |
2110a6a1bf350e11741971737cbda410394b7a5a74b1a65a33c5cc0947672333
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / tati_langchain-0.3.2-py3-none-any.whl
| Download URL | tati_langchain-0.3.2-py3-none-any.whl |
|---|---|
| Size | 26.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c93a803043534b2ac76da0f62d07f2f465f67fc840f816f7b150ed6f1f2e58e0
|
|
BLAKE2b-256 checksum How to use checksums |
dee116635856a4e53e4e8bea6b892f02dac219b1cd84ac84cd8a2855d80f4dda
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|