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 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=
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.0 → 0.1.1 |
hatch version minor |
0.1.1 → 0.2.0 |
hatch version major |
0.2.0 → 1.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
- Create a pypi.org account and API token (
pypi-...). - In the GitHub repo: Settings → Secrets and variables → Actions → New repository secret
- Name:
PYPI_API_TOKEN - Value: your PyPI API token
- Name:
Release steps
- Bump, commit, tag, and push locally (see above).
- GitHub → Actions → Publish to PyPI → Run workflow.
- In the confirm box, type exactly:
publish
(anything else skips the job).
What the Action does
- Checks out the repo
- Installs
build+hatch - Prints the version (
hatch version) - Runs
python -m build→ sdist + wheel underdist/ - Uploads to https://pypi.org via
pypa/gh-action-pypi-publishusingPYPI_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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6ba1500dab22b73b3c545e8aa19634187e90662fcb6c5044cd5ea9f8a64cf48
|
|
| MD5 |
8e9e67260aa3b96a331695f474126367
|
|
| BLAKE2b-256 |
cc8b7d0db3f8ab8e3e4342730a01745f25d2b62ebd695fa850e48381d27b4f5c
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e859e3aeb768d0602ff8d51fb527cb2e25a1a32a4304306e0bd23d92d10c15f9
|
|
| MD5 |
c8fed42791fa21322a3108a42f6a0a47
|
|
| BLAKE2b-256 |
93451ded9bd2ef1c777f5fa99e7dd345d26be25aa197c8ff1f398d0572533b65
|