tessaract 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tessaract-0.1.0/LICENSE +9 -0
- tessaract-0.1.0/PKG-INFO +277 -0
- tessaract-0.1.0/README.md +261 -0
- tessaract-0.1.0/pyproject.toml +24 -0
- tessaract-0.1.0/pyproject.toml.orig +29 -0
- tessaract-0.1.0/src/tessaract/__init__.py +16 -0
- tessaract-0.1.0/src/tessaract/adapters/__init__.py +3 -0
- tessaract-0.1.0/src/tessaract/adapters/adapter.py +55 -0
- tessaract-0.1.0/src/tessaract/adapters/openai/openai_adapter.py +348 -0
- tessaract-0.1.0/src/tessaract/client.py +151 -0
- tessaract-0.1.0/src/tessaract/providers/__init__.py +7 -0
- tessaract-0.1.0/src/tessaract/providers/openai_provider.py +35 -0
- tessaract-0.1.0/src/tessaract/providers/provider.py +36 -0
- tessaract-0.1.0/src/tessaract/tools/__init__.py +7 -0
- tessaract-0.1.0/src/tessaract/tools/function.py +71 -0
- tessaract-0.1.0/src/tessaract/types/__init__.py +1 -0
- tessaract-0.1.0/src/tessaract/types/input_types.py +27 -0
- tessaract-0.1.0/src/tessaract/types/output_types.py +60 -0
- tessaract-0.1.0/src/tessaract/types/request.py +33 -0
- tessaract-0.1.0/src/tessaract/types/response.py +73 -0
- tessaract-0.1.0/src/tessaract/types/streaming/event_types.py +146 -0
- tessaract-0.1.0/src/tessaract/types/types.py +0 -0
tessaract-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jennifer Umoke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
tessaract-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tessaract
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic SDK for building AI agents natively
|
|
5
|
+
Author: Jennifer Umoke
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pydantic>=2.10,<3
|
|
9
|
+
Requires-Dist: openai>=2.45.0 ; extra == 'openai'
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Project-URL: Repository, https://github.com/Chinenyay/tessaract
|
|
12
|
+
Project-URL: Issues, https://github.com/Chinenyay/tessaract/issues
|
|
13
|
+
Project-URL: Documentation, https://github.com/Chinenyay/tessaract/blob/main/README.md
|
|
14
|
+
Provides-Extra: openai
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Tessaract
|
|
18
|
+
|
|
19
|
+
A provider-agnostic SDK for building AI agents natively.
|
|
20
|
+
|
|
21
|
+
Tessaract exists to make it straightforward to build agents that work across multiple providers. It is an unopinionated portal to multi-model intelligence giving you model diversity and full control over how you compose models in your applications.
|
|
22
|
+
|
|
23
|
+
Today, Tessaract supports OpenAI's Responses API, including streaming, reasoning, and tool calling. More provider adapters are planned.
|
|
24
|
+
|
|
25
|
+
More concretely, Tessaract exposes a **canonical model of the LLM ecosystem**: requests, responses, content, streaming events, tools, usage, errors and reasoning. You write an agent loop once against Tessaract's types, and a provider **adapter** translates to and from each vendor's native API.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
> **Status:** OpenAI is the only supported provider today. Anthropic support is in progress.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Contents
|
|
33
|
+
|
|
34
|
+
- [Features](#features)
|
|
35
|
+
- [Installation](#installation)
|
|
36
|
+
- [Quickstart](#quickstart)
|
|
37
|
+
- [Build an agent with reasoning and tool calling](#build-an-agent-with-reasoning-and-tool-calling)
|
|
38
|
+
- [Streaming](#streaming)
|
|
39
|
+
- [Documentation](#documentation)
|
|
40
|
+
- [Project layout](#project-layout)
|
|
41
|
+
- [Status and roadmap](#status-and-roadmap)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Provider prefixes.** Register a provider under a prefix and address models as `"<prefix>/<model>"`, e.g. `"oai/gpt-5.6-luna"`.
|
|
48
|
+
- **Canonical tools.** Define function tools once with `FunctionTool` / `InputSchema` / `Property`, including nested objects, arrays, enums and nullable fields. The adapter emits the provider's native schema.
|
|
49
|
+
- **Canonical reasoning.** Control effort and summaries with `ReasoningOptions`; read reasoning back as `ReasoningOutputItem`s.
|
|
50
|
+
- **Typed output.** Responses contain `AssistantMessage`, `ReasoningOutputItem`, `FunctionCallOutputItem` and `ProviderOutputItem` objects. Function-call arguments are already parsed into a `dict`.
|
|
51
|
+
- **Canonical streaming events.** Text, reasoning and tool-argument deltas are normalized into typed events (`text.delta`, `reasoning_summary.delta`, `response.completed`, …).
|
|
52
|
+
- **Lossless multi-turn history.** Every output item keeps its provider-native `raw` payload, so you can append a response's output straight back into the conversation history.
|
|
53
|
+
- **Escape hatches everywhere.** `request_options` passes any native request parameter through, `FunctionTool.provider_options` adds native tool fields, and `raw_response` / `raw_event` expose the original SDK objects.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
Tessaract requires **Python 3.11+**. Install the `openai` extra to use the currently supported provider.
|
|
60
|
+
|
|
61
|
+
With pip:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install "tessaract[openai]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With uv:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv add "tessaract[openai]"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
Set your OpenAI API key:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export OPENAI_API_KEY="your-api-key"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then send your first request:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from tessaract import OpenAIProvider, Tessaract
|
|
87
|
+
|
|
88
|
+
client = Tessaract(providers={"oai": OpenAIProvider()})
|
|
89
|
+
response = client.send(model="oai/gpt-5.6-luna", input="Say hello in five words.")
|
|
90
|
+
|
|
91
|
+
print(response.output_text)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`OpenAIProvider` reads `OPENAI_API_KEY` from the environment. The `oai` prefix is your choice; Tessaract sends the model name after `/` to OpenAI. See the [getting started guide](https://github.com/Chinenyay/tessaract/blob/main/docs/getting-started.md) for configuration and multi-turn conversations.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Build an agent with reasoning and tool calling
|
|
99
|
+
|
|
100
|
+
An agent is a loop:
|
|
101
|
+
|
|
102
|
+
1. Send the conversation history, plus the tools the model may use.
|
|
103
|
+
2. Append the model's output (reasoning, messages and function calls) to the history.
|
|
104
|
+
3. If the model asked for function calls, run them and append a `FunctionToolResult` for each one.
|
|
105
|
+
4. Repeat until the model replies without calling a tool.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import json
|
|
109
|
+
import os
|
|
110
|
+
from datetime import datetime, timezone
|
|
111
|
+
|
|
112
|
+
from tessaract import (
|
|
113
|
+
FunctionTool,
|
|
114
|
+
FunctionToolResult,
|
|
115
|
+
InputSchema,
|
|
116
|
+
OpenAIProvider,
|
|
117
|
+
Property,
|
|
118
|
+
ReasoningOptions,
|
|
119
|
+
Tessaract,
|
|
120
|
+
UserMessage,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# 1. Plain Python functions the agent can call
|
|
124
|
+
def get_time() -> str:
|
|
125
|
+
return datetime.now(timezone.utc).isoformat()
|
|
126
|
+
|
|
127
|
+
def get_weather(city: str) -> str:
|
|
128
|
+
temps = {"paris": "19C", "amsterdam": "20C"}
|
|
129
|
+
return json.dumps({"temperature": temps.get(city.lower(), "unknown")})
|
|
130
|
+
|
|
131
|
+
FUNCTIONS = {"get_time": get_time, "get_weather": get_weather}
|
|
132
|
+
|
|
133
|
+
# 2. Describe them to the model
|
|
134
|
+
tools = [
|
|
135
|
+
FunctionTool(
|
|
136
|
+
name="get_time",
|
|
137
|
+
description="Get the current UTC time as an ISO-8601 string.",
|
|
138
|
+
strict=False,
|
|
139
|
+
),
|
|
140
|
+
FunctionTool(
|
|
141
|
+
name="get_weather",
|
|
142
|
+
description="Get the current temperature for a city.",
|
|
143
|
+
input_schema=InputSchema(
|
|
144
|
+
properties={
|
|
145
|
+
"city": Property(type="string", description="City name, e.g. Paris."),
|
|
146
|
+
},
|
|
147
|
+
required=["city"],
|
|
148
|
+
additionalProperties=False,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
client = Tessaract(
|
|
154
|
+
providers={"oai": OpenAIProvider(api_key=os.environ["OPENAI_API_KEY"])}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# 3. The agent loop
|
|
158
|
+
def run_agent(history: list, user_text: str) -> str:
|
|
159
|
+
history.append(UserMessage(content=user_text))
|
|
160
|
+
|
|
161
|
+
while True:
|
|
162
|
+
response = client.send(
|
|
163
|
+
model="oai/gpt-5.6-luna",
|
|
164
|
+
input=history,
|
|
165
|
+
tools=tools,
|
|
166
|
+
reasoning=ReasoningOptions(effort="medium", summary="auto"),
|
|
167
|
+
request_options={"instructions": "You are a concise, helpful assistant."},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Keep reasoning, messages and calls in the history for the next turn
|
|
171
|
+
history.extend(response.output)
|
|
172
|
+
|
|
173
|
+
for item in response.output:
|
|
174
|
+
if item.type == "reasoning" and item.text:
|
|
175
|
+
print(f"[thinking] {item.text}")
|
|
176
|
+
|
|
177
|
+
calls = [item for item in response.output if item.type == "function_call"]
|
|
178
|
+
if not calls:
|
|
179
|
+
return response.output_text
|
|
180
|
+
|
|
181
|
+
for call in calls:
|
|
182
|
+
result = FUNCTIONS[call.name](**call.arguments) # arguments is already a dict
|
|
183
|
+
history.append(FunctionToolResult(call_id=call.call_id, result=result))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
history: list = []
|
|
187
|
+
print(run_agent(history, "What's the weather in Paris, and what time is it?"))
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The step-by-step walkthrough is in the [building an agent guide](https://github.com/Chinenyay/tessaract/blob/main/docs/building-an-agent.md), with runnable versions in [`examples/`](https://github.com/Chinenyay/tessaract/tree/main/examples).
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Streaming
|
|
195
|
+
|
|
196
|
+
Pass `stream=True` to get an iterator of canonical events. The final event is `response.completed`, which carries the same `Response` object that a non-streaming call returns, so the agent loop doesn't change:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
completed = None
|
|
200
|
+
|
|
201
|
+
for event in client.send(
|
|
202
|
+
model="oai/gpt-5.6-luna",
|
|
203
|
+
input=history,
|
|
204
|
+
tools=tools,
|
|
205
|
+
stream=True,
|
|
206
|
+
reasoning=ReasoningOptions(effort="medium", summary="auto"),
|
|
207
|
+
):
|
|
208
|
+
if event.type == "reasoning.started":
|
|
209
|
+
print("\n[thinking] ", end="")
|
|
210
|
+
elif event.type == "reasoning_summary.delta":
|
|
211
|
+
print(event.delta, end="", flush=True)
|
|
212
|
+
elif event.type == "text.delta":
|
|
213
|
+
print(event.delta, end="", flush=True)
|
|
214
|
+
elif event.type == "response.completed":
|
|
215
|
+
completed = event.response
|
|
216
|
+
|
|
217
|
+
history.extend(completed.output)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
See the [streaming guide](https://github.com/Chinenyay/tessaract/blob/main/docs/streaming.md) for the full event list.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Documentation
|
|
225
|
+
|
|
226
|
+
| Guide | What it covers |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| [Getting started](https://github.com/Chinenyay/tessaract/blob/main/docs/getting-started.md) | Installing, configuring providers, sending your first request, and building multi-turn history |
|
|
229
|
+
| [Building an agent](https://github.com/Chinenyay/tessaract/blob/main/docs/building-an-agent.md) | A step-by-step tutorial for an agent with reasoning and tool calling, both synchronous and streaming |
|
|
230
|
+
| [Tool calling](https://github.com/Chinenyay/tessaract/blob/main/docs/tool-calling.md) | `FunctionTool`, `InputSchema`, `Property`, strict mode, tool results and `provider_options` |
|
|
231
|
+
| [Reasoning](https://github.com/Chinenyay/tessaract/blob/main/docs/reasoning.md) | `ReasoningOptions`, effort levels, summaries, and reading and preserving reasoning items |
|
|
232
|
+
| [Streaming](https://github.com/Chinenyay/tessaract/blob/main/docs/streaming.md) | Every canonical stream event, and how to build a streaming agent loop |
|
|
233
|
+
| [API reference](https://github.com/Chinenyay/tessaract/blob/main/docs/api-reference.md) | Every public class, field and method |
|
|
234
|
+
| [Architecture](https://github.com/Chinenyay/tessaract/blob/main/docs/architecture.md) | Canonical types, adapters and providers, and how to add a new provider |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Project layout
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
src/tessaract/
|
|
242
|
+
├── client.py # Tessaract client: routes requests to adapters
|
|
243
|
+
├── providers/
|
|
244
|
+
│ ├── provider.py # Provider base dataclass
|
|
245
|
+
│ └── openai_provider.py # OpenAIProvider (wraps openai.OpenAI)
|
|
246
|
+
├── adapters/
|
|
247
|
+
│ ├── adapter.py # Adapter base class + protocols
|
|
248
|
+
│ └── openai/openai_adapter.py # Canonical <-> OpenAI Responses API translation
|
|
249
|
+
├── tools/function.py # FunctionTool, InputSchema, Property
|
|
250
|
+
└── types/
|
|
251
|
+
├── input_types.py # UserMessage, FunctionToolResult
|
|
252
|
+
├── output_types.py # AssistantMessage, ReasoningOutputItem, FunctionCallOutputItem, ...
|
|
253
|
+
├── request.py # Request, ReasoningOptions
|
|
254
|
+
├── response.py # Response, ResponseStatus, ResponseError
|
|
255
|
+
└── streaming/event_types.py # Canonical stream events
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Status and roadmap
|
|
261
|
+
|
|
262
|
+
**Working today (OpenAI):**
|
|
263
|
+
|
|
264
|
+
- Synchronous and streaming requests via the Responses API
|
|
265
|
+
- Multi-turn conversations
|
|
266
|
+
- Function tools with parallel calls
|
|
267
|
+
- Reasoning effort and summaries
|
|
268
|
+
- Pass-through request options
|
|
269
|
+
|
|
270
|
+
**Planned:**
|
|
271
|
+
|
|
272
|
+
- Anthropic provider and adapter
|
|
273
|
+
- Google GenAI provider and adapter
|
|
274
|
+
- Token usage and finish details on `Response`
|
|
275
|
+
- TestProvider for CI and testing without making live API calls
|
|
276
|
+
- built-in utils: agent loop helper, tool schema autowriter
|
|
277
|
+
- Token counting
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Tessaract
|
|
2
|
+
|
|
3
|
+
A provider-agnostic SDK for building AI agents natively.
|
|
4
|
+
|
|
5
|
+
Tessaract exists to make it straightforward to build agents that work across multiple providers. It is an unopinionated portal to multi-model intelligence giving you model diversity and full control over how you compose models in your applications.
|
|
6
|
+
|
|
7
|
+
Today, Tessaract supports OpenAI's Responses API, including streaming, reasoning, and tool calling. More provider adapters are planned.
|
|
8
|
+
|
|
9
|
+
More concretely, Tessaract exposes a **canonical model of the LLM ecosystem**: requests, responses, content, streaming events, tools, usage, errors and reasoning. You write an agent loop once against Tessaract's types, and a provider **adapter** translates to and from each vendor's native API.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
> **Status:** OpenAI is the only supported provider today. Anthropic support is in progress.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quickstart](#quickstart)
|
|
21
|
+
- [Build an agent with reasoning and tool calling](#build-an-agent-with-reasoning-and-tool-calling)
|
|
22
|
+
- [Streaming](#streaming)
|
|
23
|
+
- [Documentation](#documentation)
|
|
24
|
+
- [Project layout](#project-layout)
|
|
25
|
+
- [Status and roadmap](#status-and-roadmap)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Provider prefixes.** Register a provider under a prefix and address models as `"<prefix>/<model>"`, e.g. `"oai/gpt-5.6-luna"`.
|
|
32
|
+
- **Canonical tools.** Define function tools once with `FunctionTool` / `InputSchema` / `Property`, including nested objects, arrays, enums and nullable fields. The adapter emits the provider's native schema.
|
|
33
|
+
- **Canonical reasoning.** Control effort and summaries with `ReasoningOptions`; read reasoning back as `ReasoningOutputItem`s.
|
|
34
|
+
- **Typed output.** Responses contain `AssistantMessage`, `ReasoningOutputItem`, `FunctionCallOutputItem` and `ProviderOutputItem` objects. Function-call arguments are already parsed into a `dict`.
|
|
35
|
+
- **Canonical streaming events.** Text, reasoning and tool-argument deltas are normalized into typed events (`text.delta`, `reasoning_summary.delta`, `response.completed`, …).
|
|
36
|
+
- **Lossless multi-turn history.** Every output item keeps its provider-native `raw` payload, so you can append a response's output straight back into the conversation history.
|
|
37
|
+
- **Escape hatches everywhere.** `request_options` passes any native request parameter through, `FunctionTool.provider_options` adds native tool fields, and `raw_response` / `raw_event` expose the original SDK objects.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Tessaract requires **Python 3.11+**. Install the `openai` extra to use the currently supported provider.
|
|
44
|
+
|
|
45
|
+
With pip:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install "tessaract[openai]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With uv:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add "tessaract[openai]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
Set your OpenAI API key:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export OPENAI_API_KEY="your-api-key"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then send your first request:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from tessaract import OpenAIProvider, Tessaract
|
|
71
|
+
|
|
72
|
+
client = Tessaract(providers={"oai": OpenAIProvider()})
|
|
73
|
+
response = client.send(model="oai/gpt-5.6-luna", input="Say hello in five words.")
|
|
74
|
+
|
|
75
|
+
print(response.output_text)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`OpenAIProvider` reads `OPENAI_API_KEY` from the environment. The `oai` prefix is your choice; Tessaract sends the model name after `/` to OpenAI. See the [getting started guide](https://github.com/Chinenyay/tessaract/blob/main/docs/getting-started.md) for configuration and multi-turn conversations.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Build an agent with reasoning and tool calling
|
|
83
|
+
|
|
84
|
+
An agent is a loop:
|
|
85
|
+
|
|
86
|
+
1. Send the conversation history, plus the tools the model may use.
|
|
87
|
+
2. Append the model's output (reasoning, messages and function calls) to the history.
|
|
88
|
+
3. If the model asked for function calls, run them and append a `FunctionToolResult` for each one.
|
|
89
|
+
4. Repeat until the model replies without calling a tool.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import json
|
|
93
|
+
import os
|
|
94
|
+
from datetime import datetime, timezone
|
|
95
|
+
|
|
96
|
+
from tessaract import (
|
|
97
|
+
FunctionTool,
|
|
98
|
+
FunctionToolResult,
|
|
99
|
+
InputSchema,
|
|
100
|
+
OpenAIProvider,
|
|
101
|
+
Property,
|
|
102
|
+
ReasoningOptions,
|
|
103
|
+
Tessaract,
|
|
104
|
+
UserMessage,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 1. Plain Python functions the agent can call
|
|
108
|
+
def get_time() -> str:
|
|
109
|
+
return datetime.now(timezone.utc).isoformat()
|
|
110
|
+
|
|
111
|
+
def get_weather(city: str) -> str:
|
|
112
|
+
temps = {"paris": "19C", "amsterdam": "20C"}
|
|
113
|
+
return json.dumps({"temperature": temps.get(city.lower(), "unknown")})
|
|
114
|
+
|
|
115
|
+
FUNCTIONS = {"get_time": get_time, "get_weather": get_weather}
|
|
116
|
+
|
|
117
|
+
# 2. Describe them to the model
|
|
118
|
+
tools = [
|
|
119
|
+
FunctionTool(
|
|
120
|
+
name="get_time",
|
|
121
|
+
description="Get the current UTC time as an ISO-8601 string.",
|
|
122
|
+
strict=False,
|
|
123
|
+
),
|
|
124
|
+
FunctionTool(
|
|
125
|
+
name="get_weather",
|
|
126
|
+
description="Get the current temperature for a city.",
|
|
127
|
+
input_schema=InputSchema(
|
|
128
|
+
properties={
|
|
129
|
+
"city": Property(type="string", description="City name, e.g. Paris."),
|
|
130
|
+
},
|
|
131
|
+
required=["city"],
|
|
132
|
+
additionalProperties=False,
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
client = Tessaract(
|
|
138
|
+
providers={"oai": OpenAIProvider(api_key=os.environ["OPENAI_API_KEY"])}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# 3. The agent loop
|
|
142
|
+
def run_agent(history: list, user_text: str) -> str:
|
|
143
|
+
history.append(UserMessage(content=user_text))
|
|
144
|
+
|
|
145
|
+
while True:
|
|
146
|
+
response = client.send(
|
|
147
|
+
model="oai/gpt-5.6-luna",
|
|
148
|
+
input=history,
|
|
149
|
+
tools=tools,
|
|
150
|
+
reasoning=ReasoningOptions(effort="medium", summary="auto"),
|
|
151
|
+
request_options={"instructions": "You are a concise, helpful assistant."},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Keep reasoning, messages and calls in the history for the next turn
|
|
155
|
+
history.extend(response.output)
|
|
156
|
+
|
|
157
|
+
for item in response.output:
|
|
158
|
+
if item.type == "reasoning" and item.text:
|
|
159
|
+
print(f"[thinking] {item.text}")
|
|
160
|
+
|
|
161
|
+
calls = [item for item in response.output if item.type == "function_call"]
|
|
162
|
+
if not calls:
|
|
163
|
+
return response.output_text
|
|
164
|
+
|
|
165
|
+
for call in calls:
|
|
166
|
+
result = FUNCTIONS[call.name](**call.arguments) # arguments is already a dict
|
|
167
|
+
history.append(FunctionToolResult(call_id=call.call_id, result=result))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
history: list = []
|
|
171
|
+
print(run_agent(history, "What's the weather in Paris, and what time is it?"))
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The step-by-step walkthrough is in the [building an agent guide](https://github.com/Chinenyay/tessaract/blob/main/docs/building-an-agent.md), with runnable versions in [`examples/`](https://github.com/Chinenyay/tessaract/tree/main/examples).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Streaming
|
|
179
|
+
|
|
180
|
+
Pass `stream=True` to get an iterator of canonical events. The final event is `response.completed`, which carries the same `Response` object that a non-streaming call returns, so the agent loop doesn't change:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
completed = None
|
|
184
|
+
|
|
185
|
+
for event in client.send(
|
|
186
|
+
model="oai/gpt-5.6-luna",
|
|
187
|
+
input=history,
|
|
188
|
+
tools=tools,
|
|
189
|
+
stream=True,
|
|
190
|
+
reasoning=ReasoningOptions(effort="medium", summary="auto"),
|
|
191
|
+
):
|
|
192
|
+
if event.type == "reasoning.started":
|
|
193
|
+
print("\n[thinking] ", end="")
|
|
194
|
+
elif event.type == "reasoning_summary.delta":
|
|
195
|
+
print(event.delta, end="", flush=True)
|
|
196
|
+
elif event.type == "text.delta":
|
|
197
|
+
print(event.delta, end="", flush=True)
|
|
198
|
+
elif event.type == "response.completed":
|
|
199
|
+
completed = event.response
|
|
200
|
+
|
|
201
|
+
history.extend(completed.output)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
See the [streaming guide](https://github.com/Chinenyay/tessaract/blob/main/docs/streaming.md) for the full event list.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Documentation
|
|
209
|
+
|
|
210
|
+
| Guide | What it covers |
|
|
211
|
+
| --- | --- |
|
|
212
|
+
| [Getting started](https://github.com/Chinenyay/tessaract/blob/main/docs/getting-started.md) | Installing, configuring providers, sending your first request, and building multi-turn history |
|
|
213
|
+
| [Building an agent](https://github.com/Chinenyay/tessaract/blob/main/docs/building-an-agent.md) | A step-by-step tutorial for an agent with reasoning and tool calling, both synchronous and streaming |
|
|
214
|
+
| [Tool calling](https://github.com/Chinenyay/tessaract/blob/main/docs/tool-calling.md) | `FunctionTool`, `InputSchema`, `Property`, strict mode, tool results and `provider_options` |
|
|
215
|
+
| [Reasoning](https://github.com/Chinenyay/tessaract/blob/main/docs/reasoning.md) | `ReasoningOptions`, effort levels, summaries, and reading and preserving reasoning items |
|
|
216
|
+
| [Streaming](https://github.com/Chinenyay/tessaract/blob/main/docs/streaming.md) | Every canonical stream event, and how to build a streaming agent loop |
|
|
217
|
+
| [API reference](https://github.com/Chinenyay/tessaract/blob/main/docs/api-reference.md) | Every public class, field and method |
|
|
218
|
+
| [Architecture](https://github.com/Chinenyay/tessaract/blob/main/docs/architecture.md) | Canonical types, adapters and providers, and how to add a new provider |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Project layout
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
src/tessaract/
|
|
226
|
+
├── client.py # Tessaract client: routes requests to adapters
|
|
227
|
+
├── providers/
|
|
228
|
+
│ ├── provider.py # Provider base dataclass
|
|
229
|
+
│ └── openai_provider.py # OpenAIProvider (wraps openai.OpenAI)
|
|
230
|
+
├── adapters/
|
|
231
|
+
│ ├── adapter.py # Adapter base class + protocols
|
|
232
|
+
│ └── openai/openai_adapter.py # Canonical <-> OpenAI Responses API translation
|
|
233
|
+
├── tools/function.py # FunctionTool, InputSchema, Property
|
|
234
|
+
└── types/
|
|
235
|
+
├── input_types.py # UserMessage, FunctionToolResult
|
|
236
|
+
├── output_types.py # AssistantMessage, ReasoningOutputItem, FunctionCallOutputItem, ...
|
|
237
|
+
├── request.py # Request, ReasoningOptions
|
|
238
|
+
├── response.py # Response, ResponseStatus, ResponseError
|
|
239
|
+
└── streaming/event_types.py # Canonical stream events
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Status and roadmap
|
|
245
|
+
|
|
246
|
+
**Working today (OpenAI):**
|
|
247
|
+
|
|
248
|
+
- Synchronous and streaming requests via the Responses API
|
|
249
|
+
- Multi-turn conversations
|
|
250
|
+
- Function tools with parallel calls
|
|
251
|
+
- Reasoning effort and summaries
|
|
252
|
+
- Pass-through request options
|
|
253
|
+
|
|
254
|
+
**Planned:**
|
|
255
|
+
|
|
256
|
+
- Anthropic provider and adapter
|
|
257
|
+
- Google GenAI provider and adapter
|
|
258
|
+
- Token usage and finish details on `Response`
|
|
259
|
+
- TestProvider for CI and testing without making live API calls
|
|
260
|
+
- built-in utils: agent loop helper, tool schema autowriter
|
|
261
|
+
- Token counting
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tessaract"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Provider-agnostic SDK for building AI agents natively"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = ["pydantic>=2.10,<3"]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
|
|
11
|
+
[[project.authors]]
|
|
12
|
+
name = "Jennifer Umoke"
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
openai = ["openai>=2.45.0"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Repository = "https://github.com/Chinenyay/tessaract"
|
|
19
|
+
Issues = "https://github.com/Chinenyay/tessaract/issues"
|
|
20
|
+
Documentation = "https://github.com/Chinenyay/tessaract/blob/main/README.md"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.12.13, <0.13"]
|
|
24
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tessaract"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Provider-agnostic SDK for building AI agents natively"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pydantic>=2.10,<3"
|
|
9
|
+
]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ "name" = "Jennifer Umoke" },
|
|
14
|
+
]
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
openai = [
|
|
17
|
+
"openai>=2.45.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Repository = "https://github.com/Chinenyay/tessaract"
|
|
23
|
+
Issues = "https://github.com/Chinenyay/tessaract/issues"
|
|
24
|
+
Documentation = "https://github.com/Chinenyay/tessaract/blob/main/README.md"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.12.13, <0.13"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .client import Tessaract
|
|
2
|
+
from .providers.openai_provider import OpenAIProvider
|
|
3
|
+
from .tools.function import FunctionTool, InputSchema, Property
|
|
4
|
+
from .types.input_types import FunctionToolResult, UserMessage
|
|
5
|
+
from .types.request import ReasoningOptions
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"FunctionTool",
|
|
9
|
+
"FunctionToolResult",
|
|
10
|
+
"InputSchema",
|
|
11
|
+
"OpenAIProvider",
|
|
12
|
+
"Property",
|
|
13
|
+
"ReasoningOptions",
|
|
14
|
+
"Tessaract",
|
|
15
|
+
"UserMessage"
|
|
16
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Protocol
|
|
4
|
+
|
|
5
|
+
from ..providers.provider import Provider
|
|
6
|
+
from ..tools.function import InputSchema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserMessageProtocol(Protocol):
|
|
10
|
+
role: Literal["user"] = "user"
|
|
11
|
+
content: str | list[dict]
|
|
12
|
+
|
|
13
|
+
class FunctionToolResultProtocol(Protocol):
|
|
14
|
+
type: Literal["function_tool_result"] = "function_tool_result"
|
|
15
|
+
call_id: str
|
|
16
|
+
result: Any
|
|
17
|
+
is_error: bool = False
|
|
18
|
+
|
|
19
|
+
class FunctionToolSchemaProtocol(Protocol):
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
input_schema: InputSchema | None
|
|
23
|
+
strict: bool | None = None
|
|
24
|
+
provider_options: dict[str, Any]
|
|
25
|
+
|
|
26
|
+
class ReasoningParamsProtocol(Protocol):
|
|
27
|
+
effort: Literal["none", "minimal", "low", "medium", "high", "extra_high", "max"] | None = None
|
|
28
|
+
summary: Literal["concise", "auto", "detailed"] | None = None
|
|
29
|
+
mode: Literal["standard", "pro"] | None = None
|
|
30
|
+
|
|
31
|
+
class ReasoningProtocol(Protocol):
|
|
32
|
+
type: Literal["reasoning"] = "reasoning"
|
|
33
|
+
text: str | None = None
|
|
34
|
+
|
|
35
|
+
class Adapter:
|
|
36
|
+
def __init__(self, provider: Provider):
|
|
37
|
+
self._provider = provider
|
|
38
|
+
|
|
39
|
+
def map_input_message(self, item: UserMessageProtocol) -> Any:
|
|
40
|
+
raise NotImplementedError("not yet implemented...")
|
|
41
|
+
|
|
42
|
+
def map_tool_result(self, item: FunctionToolResultProtocol) -> Any:
|
|
43
|
+
raise NotImplementedError("not yet implemented...")
|
|
44
|
+
|
|
45
|
+
def map_function_schema(self, tools: list[FunctionToolSchemaProtocol]) -> list:
|
|
46
|
+
raise NotImplementedError("not yet implemented...")
|
|
47
|
+
|
|
48
|
+
def map_reasoning_params(self, item: ReasoningParamsProtocol) -> Any:
|
|
49
|
+
raise NotImplementedError("not yet implemented...")
|
|
50
|
+
|
|
51
|
+
def map_reasoning(self, item: ReasoningProtocol) -> Any:
|
|
52
|
+
raise NotImplementedError("not yet implemented")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|