Skip to main content

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 with run_tool_loop)
  • bundle.raw_llm — unbound model (use with generate_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.

Download files

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

Source Distribution

tati_langchain-0.3.2.tar.gz (29.8 kB view details)

Uploaded Source

Built Distribution

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

tati_langchain-0.3.2-py3-none-any.whl (26.4 kB view details)

Uploaded Python 3

File details

Details for the file tati_langchain-0.3.2.tar.gz.

File metadata

  • Download URL: tati_langchain-0.3.2.tar.gz
  • Upload date:
  • Size: 29.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tati_langchain-0.3.2.tar.gz
Algorithm Hash digest
SHA256 ab88922f0238e607d0c047a0b0b6def1f82e990525d55cb474525b819bd7ab74
MD5 4c65243534c6b6d510d55c061ca6fa8e
BLAKE2b-256 2110a6a1bf350e11741971737cbda410394b7a5a74b1a65a33c5cc0947672333

See more details on using hashes here.

File details

Details for the file tati_langchain-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: tati_langchain-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 26.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tati_langchain-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 c93a803043534b2ac76da0f62d07f2f465f67fc840f816f7b150ed6f1f2e58e0
MD5 8bce41441c8f4f7e90cb9c8e03ecd8ba
BLAKE2b-256 dee116635856a4e53e4e8bea6b892f02dac219b1cd84ac84cd8a2855d80f4dda

See more details on using hashes here.

Supported by

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