Open-source Agentic AI Toolkit
`Underthesea` is:
🌊 **An Agentic AI Toolkit.** Since v9.3.0, Underthesea is an open-source Agentic AI Toolkit with built-in Vietnamese NLP capabilities. It provides multi-provider AI Agent support and a suite of Python modules for [Vietnamese Natural Language Processing](https://github.com/undertheseanlp/underthesea).
🎁 [**Support Us!**](#-support-us) Every bit of support helps us achieve our goals. Thank you so much. 💝💝💝
## Installation
```bash
$ pip install underthesea
```
## Agent
Multi-provider AI Agent with **zero external dependencies**. Communicates with LLM APIs using only Python stdlib (`urllib` + `json`) — no `openai`, `anthropic`, or `google-genai` packages required.
**Providers:** OpenAI | Azure OpenAI | Anthropic Claude | Google Gemini
### Quick Start
```bash
# Pick one provider:
$ export OPENAI_API_KEY=sk-...
# or Azure:
$ export AZURE_OPENAI_API_KEY=... && export AZURE_OPENAI_ENDPOINT=https://...
# or Anthropic:
$ export ANTHROPIC_API_KEY=sk-ant-...
# or Gemini:
$ export GOOGLE_API_KEY=...
```
```python
from underthesea.agent import Agent, LLM
agent = Agent(name="assistant", provider=LLM())
agent("Hello!")
```
### Providers
Each provider is its own class, following the [Anthropic SDK](https://docs.anthropic.com/en/api/client-sdks) pattern.
```python
from underthesea.agent import Agent, OpenAI, AzureOpenAI, Anthropic, Gemini, LLM
# OpenAI
agent = Agent(name="bot", provider=OpenAI(api_key="sk-..."))
# Azure OpenAI
agent = Agent(name="bot", provider=AzureOpenAI(
api_key="...",
endpoint="https://my.openai.azure.com",
deployment="gpt-4",
))
# Anthropic Claude
agent = Agent(name="bot", provider=Anthropic(api_key="sk-ant-..."))
# Google Gemini
agent = Agent(name="bot", provider=Gemini(api_key="..."))
# Auto-detect from environment variables
agent = Agent(name="bot", provider=LLM())
```
### Streaming
```python
for chunk in agent.stream("Explain what an AI agent is"):
print(chunk, end="", flush=True)
```
### Tool Calling
```python
from underthesea.agent import Agent, Tool, OpenAI
def get_weather(location: str) -> dict:
"""Get current weather for a location."""
return {"location": location, "temp": 25, "condition": "sunny"}
agent = Agent(
name="assistant",
provider=OpenAI(),
tools=[Tool(get_weather)],
instruction="You are a helpful assistant.",
)
agent("What's the weather in Hanoi?")
# 'The weather in Hanoi is 25°C and sunny.'
```
### Default Tools
12 built-in tools: calculator, datetime, web search, wikipedia, file I/O, shell, python exec.
```python
from underthesea.agent import Agent, default_tools, LLM
agent = Agent(name="assistant", provider=LLM(), tools=default_tools)
agent("Calculate sqrt(144) + 10")
```
### Multi-Session
Long-running agents with context reset and structured handoff between sessions, following [Anthropic harness patterns](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents).
```python
from underthesea.agent import Agent, Session, AzureOpenAI
agent = Agent(name="researcher", provider=AzureOpenAI(...))
session = Session(agent, progress_file="progress.json")
session.create_task("Analyze documents", [
"Read and classify documents",
"Summarize each group",
"Write final report",
])
session.run_until_complete(max_sessions=5)
```
### Tracing
Every agent call is automatically traced to `~/.underthesea/traces/`. Disable with `UNDERTHESEA_TRACE_DISABLED=1`.
```python
from underthesea.agent import Agent, LangfuseTracer, calculator_tool
# Auto local trace (default) — zero config
agent = Agent(name="bot", tools=[calculator_tool])
agent("What is 2+2?")
# >> Trace [a1b2c3] bot
# |-- Generation: llm.chat #1 (gpt-4.1-mini) ... 1200ms | 100->18 tokens
# |-- Tool: tool.calculator ... 0ms
# |-- Generation: llm.chat #2 (gpt-4.1-mini) ... 800ms | 150->12 tokens
# << Trace [a1b2c3] [ok] 2000ms -> ~/.underthesea/traces/20260411_trace_a1b2c3.json
# Langfuse (pip install langfuse)
agent = Agent(name="bot", tools=[calculator_tool], tracer=LangfuseTracer())
# @trace decorator — nested functions become child spans
from underthesea.agent.trace import trace, LocalTracer
@trace(LocalTracer())
def pipeline(text):
return Agent(name="bot")(text) # auto-inherits trace context
```
### Serving (A2A)
Expose any agent over the [A2A protocol](https://github.com/google-agentic-commerce/AP2) — JSON-RPC `message/stream` over HTTP+SSE with a discoverable `AgentCard` and an optional bundled chat UI. The server is a raw ASGI app with **no web-framework dep** in the base install — plug it into any ASGI server you like.
```bash
# Optional convenience extra: uvicorn + starlette + httpx
$ pip install 'underthesea[agent-server]'
```
```python
from underthesea.agent import Agent, Tool
from underthesea.agent.server import serve
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
agent = Agent(name="MathAgent", instruction="...", tools=[Tool(add)])
serve(agent, port=8000, path="/a2a/math", ui=True)
# → GET http://127.0.0.1:8000/a2a/math/ui (bundled chat UI)
# → GET http://127.0.0.1:8000/a2a/math/.well-known/agent-card.json
# → POST http://127.0.0.1:8000/a2a/math (JSON-RPC message/stream, SSE)
```
For custom routing or your own ASGI server, use `make_app()`:
```python
from underthesea.agent.server import make_app
app = make_app(agent, path="/a2a/math") # → raw ASGI callable
# uvicorn module:app
# hypercorn module:app
# daphne module:app
```
One `Agent` is spawned per A2A `contextId` so each conversation keeps its own history. Tool calls are streamed live as `tool_call` artifacts; the agent's reply arrives as a `text` artifact when the loop finishes.
### Architecture
```
underthesea.agent
├── providers/
│ ├── OpenAI # api.openai.com
│ ├── AzureOpenAI # *.openai.azure.com
│ ├── Anthropic # api.anthropic.com
│ └── Gemini # generativelanguage.googleapis.com
├── trace/
│ ├── LocalTracer # JSON files to ~/.underthesea/traces/
│ ├── LangfuseTracer # Langfuse v4 observability
│ └── @trace # Decorator with auto-nesting
├── server/
│ ├── make_app # Raw ASGI callable (JSON-RPC + SSE + AgentCard)
│ ├── serve # uvicorn convenience entrypoint
│ └── static/ # Bundled chat UI (ui=True)
├── Agent # Tool calling loop + streaming
├── LLM # Auto-detect provider from env vars
├── Session # Multi-session orchestration
├── Tool # Function → tool wrapper
└── default_tools # 12 built-in tools
```
## Vietnamese NLP
See full documentation at [NLP.md](NLP.md).
| Pipeline | Usage |
|----------|-------|
| Sentence Segmentation | `sent_tokenize(text)` |
| Text Normalization | `text_normalize(text)` |
| Diacritics Restoration | `restore_diacritics(text)` |
| Word Segmentation | `word_tokenize(text)` |
| POS Tagging | `pos_tag(text)` |
| Chunking | `chunk(text)` |
| Named Entity Recognition | `ner(text)` |
| Text Classification | `classify(text)` |
| Sentiment Analysis | `sentiment(text)` |
| Language Detection | `lang_detect(text)` |
| Dependency Parsing | `dependency_parse(text)` |
| Translation | `translate(text)` |
| Text-to-Speech | `tts(text)` |
```python
from underthesea import word_tokenize, ner, sentiment
word_tokenize("Chàng trai 9X Quảng Trị khởi nghiệp từ nấm sò")
# ["Chàng trai", "9X", "Quảng Trị", "khởi nghiệp", "từ", "nấm", "sò"]
ner("Chưa tiết lộ lịch trình tới Việt Nam của Tổng thống Mỹ Donald Trump")
# [... ('Việt Nam', 'Np', 'B-NP', 'B-LOC'), ... ('Donald', 'Np', 'B-NP', 'B-PER'), ('Trump', 'Np', 'B-NP', 'I-PER')]
sentiment("Sản phẩm hơi nhỏ nhưng chất lượng tốt, đóng gói cẩn thận.")
# 'positive'
```
## Contributing
Do you want to contribute with underthesea development? Great! Please read more details at [Contributing Guide](https://undertheseanlp.github.io/underthesea/docs/developer/contributing)
## 💝 Support Us
If you found this project helpful and would like to support our work, you can just buy us a coffee ☕.
Your support is our biggest encouragement 🎁!