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

Private GitHub repo: TatiSoftware/tati-langchain. You need a GitHub PAT with contents:read on this repo. Export it first so ${GITHUB_TOKEN} expands in the install URL:

export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"

From a consuming project (requirements.in)

Add a pinned git dependency to your project's requirements.in:

tati-langchain @ git+https://tati-digital:${GITHUB_TOKEN}@github.com/TatiSoftware/tati-langchain.git@v0.2.0
# or with Bedrock:
# tati-langchain[bedrock] @ git+https://tati-digital:${GITHUB_TOKEN}@github.com/TatiSoftware/tati-langchain.git@v0.2.0

Then install:

export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
pip install -r requirements.in

(If you use pip-tools: pip-compile requirements.in && pip-sync, with the token exported in the same shell so the git URL can authenticate.)

Direct pip install

export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"

# core (OpenAI + Anthropic) — pin to a released tag
pip install "tati-langchain @ git+https://tati-digital:${GITHUB_TOKEN}@github.com/TatiSoftware/tati-langchain.git@v0.2.0"

# + AWS Bedrock
pip install "tati-langchain[bedrock] @ git+https://tati-digital:${GITHUB_TOKEN}@github.com/TatiSoftware/tati-langchain.git@v0.2.0"

tati-digital is just the username placeholder in the URL (GitHub ignores it when a token is present; keep it for clarity in CI).

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=

Development

python3 -m venv .venv && source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install hatch build
pip install -e ".[dev]"
# optional Bedrock extra while developing against it:
# pip install -e ".[dev,bedrock]"
pytest --cov=tati_langchain --cov-report=term-missing
ruff check .

The package version lives in a single place: src/tati_langchain/__about__.py. pyproject.toml reads it via Hatch (dynamic = ["version"]).

Version bumping

Hatch owns semantic version bumps:

Command From → To (example)
hatch version patch 0.1.00.1.1
hatch version minor 0.1.10.2.0
hatch version major 0.2.01.0.0
# see current version
hatch version

# bump (edits __about__.py)
hatch version patch   # or: minor / major

Releasing (git tag)

We ship by git tag. After bumping:

# 1) bump version (example: first public-ish release)
hatch version 0.1.0          # set explicitly, or use patch/minor/major

# 2) commit + tag + push
git add .
git commit -m "Release v$(hatch version)"
git tag "v$(hatch version)"
git push -u origin main
git push origin "v$(hatch version)"

Consumers then install that tag from the private repo, e.g.:

export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
pip install -r requirements.in
# or directly:
pip install "tati-langchain @ git+https://tati-digital:${GITHUB_TOKEN}@github.com/TatiSoftware/tati-langchain.git@v0.1.0"

GitHub Actions (manual only)

Both workflows are dispatch-only — they never run on push/PR automatically.

Workflow File What it does
Tests .github/workflows/tests.yml Ruff + pytest on Python 3.10 / 3.11 / 3.12
Publish to PyPI .github/workflows/publish-pypi.yml Build sdist + wheel and upload to PyPI

Run them from GitHub → Actions → pick the workflow → Run workflow.

Publishing to PyPI

The publish workflow uploads whatever version is in the repo at checkout (from __about__.py). It does not invent a version or create a git tag.

One-time setup

  1. Create a pypi.org account and API token (pypi-...).
  2. In the GitHub repo: Settings → Secrets and variables → Actions → New repository secret
    • Name: PYPI_API_TOKEN
    • Value: your PyPI API token

Release steps

  1. Bump, commit, tag, and push locally (see above).
  2. GitHub → Actions → Publish to PyPI → Run workflow.
  3. In the confirm box, type exactly: publish
    (anything else skips the job).

What the Action does

  1. Checks out the repo
  2. Installs build + hatch
  3. Prints the version (hatch version)
  4. Runs python -m build → sdist + wheel under dist/
  5. Uploads to https://pypi.org via pypa/gh-action-pypi-publish using PYPI_API_TOKEN

After it’s live

pip install tati-langchain==0.2.0

(use the version you published)

What it does not do

  • Does not run on push/tag automatically — only on dispatch
  • Does not create the git tag for you — bump + tag + push first
  • Does not publish to TestPyPI (only real PyPI)

Note: A proprietary “internal use only” license can still sit on public PyPI and be installable by anyone who knows the package name. Prefer the private GitHub install path if you only want Tati machines to pull it.

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

Uploaded Python 3

File details

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

File metadata

  • Download URL: tati_langchain-0.3.1.tar.gz
  • Upload date:
  • Size: 31.6 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.1.tar.gz
Algorithm Hash digest
SHA256 a6ba1500dab22b73b3c545e8aa19634187e90662fcb6c5044cd5ea9f8a64cf48
MD5 8e9e67260aa3b96a331695f474126367
BLAKE2b-256 cc8b7d0db3f8ab8e3e4342730a01745f25d2b62ebd695fa850e48381d27b4f5c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tati_langchain-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 28.1 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e859e3aeb768d0602ff8d51fb527cb2e25a1a32a4304306e0bd23d92d10c15f9
MD5 c8fed42791fa21322a3108a42f6a0a47
BLAKE2b-256 93451ded9bd2ef1c777f5fa99e7dd345d26be25aa197c8ff1f398d0572533b65

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