freesolo 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.
- freesolo-0.1.0/.env.example +2 -0
- freesolo-0.1.0/.gitignore +9 -0
- freesolo-0.1.0/PKG-INFO +100 -0
- freesolo-0.1.0/README.md +87 -0
- freesolo-0.1.0/examples/.env.example +6 -0
- freesolo-0.1.0/examples/README.md +47 -0
- freesolo-0.1.0/examples/__init__.py +1 -0
- freesolo-0.1.0/examples/anthropic/__init__.py +1 -0
- freesolo-0.1.0/examples/anthropic/chat.py +72 -0
- freesolo-0.1.0/examples/anthropic/vision.py +97 -0
- freesolo-0.1.0/examples/openai/__init__.py +1 -0
- freesolo-0.1.0/examples/openai/chat.py +68 -0
- freesolo-0.1.0/examples/openai/vision.py +79 -0
- freesolo-0.1.0/examples/utils.py +199 -0
- freesolo-0.1.0/pyproject.toml +28 -0
- freesolo-0.1.0/src/freesolo/__init__.py +21 -0
- freesolo-0.1.0/src/freesolo/client.py +385 -0
- freesolo-0.1.0/src/freesolo/decorators.py +62 -0
- freesolo-0.1.0/src/freesolo/providers.py +185 -0
- freesolo-0.1.0/src/freesolo/sanitize.py +123 -0
- freesolo-0.1.0/uv.lock +485 -0
freesolo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: freesolo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decorator-based LLM tracing package with OpenAI and Anthropic client instrumentation.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx>=0.27.0
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: ruff>=0.11.0; extra == 'dev'
|
|
9
|
+
Provides-Extra: examples
|
|
10
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'examples'
|
|
11
|
+
Requires-Dist: openai>=1.0.0; extra == 'examples'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# freesolo
|
|
15
|
+
|
|
16
|
+
`freesolo` is a Python tracing package for the Reinforcement Labs app.
|
|
17
|
+
|
|
18
|
+
It is designed for the lowest-friction integration possible:
|
|
19
|
+
|
|
20
|
+
1. Install the package
|
|
21
|
+
2. Configure one endpoint + API key
|
|
22
|
+
3. Start a trace once
|
|
23
|
+
4. Add `@trace()` to your functions
|
|
24
|
+
5. Optionally wrap your OpenAI, Anthropic, or Google client for automatic provider spans
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd tracing
|
|
30
|
+
uv sync
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
To run the bundled example apps:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd tracing
|
|
37
|
+
uv sync --extra examples
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Environment
|
|
41
|
+
|
|
42
|
+
The package reads these environment variables by default:
|
|
43
|
+
|
|
44
|
+
- `FREESOLO_BASE_URL`
|
|
45
|
+
- `FREESOLO_API_KEY`
|
|
46
|
+
|
|
47
|
+
The frontend app must also have:
|
|
48
|
+
|
|
49
|
+
- `FREESOLO_API_KEY`
|
|
50
|
+
- `SUPABASE_SECRET_KEY`
|
|
51
|
+
|
|
52
|
+
The canonical database schema lives in `frontend/supabase/migrations/`.
|
|
53
|
+
There is no separate tracing-side migration folder because both the ingest API
|
|
54
|
+
and the Supabase project are owned by the frontend app in this repo.
|
|
55
|
+
|
|
56
|
+
Runnable examples live in [`examples/`](./examples/README.md). They will
|
|
57
|
+
autoload env vars from `frontend/.env`, `tracing/.env`, or `tracing/examples/.env`.
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from openai import OpenAI
|
|
63
|
+
from freesolo import configure, instrument_openai, start_trace, trace
|
|
64
|
+
|
|
65
|
+
configure(
|
|
66
|
+
base_url="https://your-app.com",
|
|
67
|
+
api_key="freesolo_api_key",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
client = instrument_openai(OpenAI())
|
|
71
|
+
|
|
72
|
+
@trace()
|
|
73
|
+
def run_agent(question: str) -> str:
|
|
74
|
+
result = client.chat.completions.create(
|
|
75
|
+
model="gpt-4.1",
|
|
76
|
+
messages=[
|
|
77
|
+
{"role": "user", "content": question},
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
return result.choices[0].message.content or ""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
with start_trace("support-agent-run"):
|
|
84
|
+
print(run_agent("How do I reset my password?"))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What Gets Stored
|
|
88
|
+
|
|
89
|
+
- Trace metadata if you explicitly pass it to `start_trace(..., metadata=...)`
|
|
90
|
+
- Spans from decorators
|
|
91
|
+
- OpenAI / Anthropic / Google request + response payloads
|
|
92
|
+
- Token usage when available
|
|
93
|
+
- Image inputs summarized safely instead of storing full base64 blobs
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
|
|
97
|
+
- If you do nothing except add `@trace()`, you still get useful spans.
|
|
98
|
+
- Use `start_trace(trace_id=...)` when you want several decorated functions to land in one trace.
|
|
99
|
+
- If you also wrap the LLM client, you get dedicated provider/model spans.
|
|
100
|
+
- Delivery is best-effort by default. Trace ingestion failures do not break your app.
|
freesolo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# freesolo
|
|
2
|
+
|
|
3
|
+
`freesolo` is a Python tracing package for the Reinforcement Labs app.
|
|
4
|
+
|
|
5
|
+
It is designed for the lowest-friction integration possible:
|
|
6
|
+
|
|
7
|
+
1. Install the package
|
|
8
|
+
2. Configure one endpoint + API key
|
|
9
|
+
3. Start a trace once
|
|
10
|
+
4. Add `@trace()` to your functions
|
|
11
|
+
5. Optionally wrap your OpenAI, Anthropic, or Google client for automatic provider spans
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd tracing
|
|
17
|
+
uv sync
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
To run the bundled example apps:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd tracing
|
|
24
|
+
uv sync --extra examples
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Environment
|
|
28
|
+
|
|
29
|
+
The package reads these environment variables by default:
|
|
30
|
+
|
|
31
|
+
- `FREESOLO_BASE_URL`
|
|
32
|
+
- `FREESOLO_API_KEY`
|
|
33
|
+
|
|
34
|
+
The frontend app must also have:
|
|
35
|
+
|
|
36
|
+
- `FREESOLO_API_KEY`
|
|
37
|
+
- `SUPABASE_SECRET_KEY`
|
|
38
|
+
|
|
39
|
+
The canonical database schema lives in `frontend/supabase/migrations/`.
|
|
40
|
+
There is no separate tracing-side migration folder because both the ingest API
|
|
41
|
+
and the Supabase project are owned by the frontend app in this repo.
|
|
42
|
+
|
|
43
|
+
Runnable examples live in [`examples/`](./examples/README.md). They will
|
|
44
|
+
autoload env vars from `frontend/.env`, `tracing/.env`, or `tracing/examples/.env`.
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from openai import OpenAI
|
|
50
|
+
from freesolo import configure, instrument_openai, start_trace, trace
|
|
51
|
+
|
|
52
|
+
configure(
|
|
53
|
+
base_url="https://your-app.com",
|
|
54
|
+
api_key="freesolo_api_key",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
client = instrument_openai(OpenAI())
|
|
58
|
+
|
|
59
|
+
@trace()
|
|
60
|
+
def run_agent(question: str) -> str:
|
|
61
|
+
result = client.chat.completions.create(
|
|
62
|
+
model="gpt-4.1",
|
|
63
|
+
messages=[
|
|
64
|
+
{"role": "user", "content": question},
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
return result.choices[0].message.content or ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
with start_trace("support-agent-run"):
|
|
71
|
+
print(run_agent("How do I reset my password?"))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What Gets Stored
|
|
75
|
+
|
|
76
|
+
- Trace metadata if you explicitly pass it to `start_trace(..., metadata=...)`
|
|
77
|
+
- Spans from decorators
|
|
78
|
+
- OpenAI / Anthropic / Google request + response payloads
|
|
79
|
+
- Token usage when available
|
|
80
|
+
- Image inputs summarized safely instead of storing full base64 blobs
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- If you do nothing except add `@trace()`, you still get useful spans.
|
|
85
|
+
- Use `start_trace(trace_id=...)` when you want several decorated functions to land in one trace.
|
|
86
|
+
- If you also wrap the LLM client, you get dedicated provider/model spans.
|
|
87
|
+
- Delivery is best-effort by default. Trace ingestion failures do not break your app.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
These examples show the intended integration shape:
|
|
4
|
+
|
|
5
|
+
1. install `freesolo`
|
|
6
|
+
2. wrap the provider client once
|
|
7
|
+
3. start a trace once
|
|
8
|
+
4. add `@trace()` to your own workflow functions
|
|
9
|
+
5. let the package emit traces to the frontend ingest API
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd tracing
|
|
15
|
+
uv sync --extra examples
|
|
16
|
+
cp examples/.env.example examples/.env
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The scripts load env vars in this order:
|
|
20
|
+
|
|
21
|
+
1. `frontend/.env`
|
|
22
|
+
2. `frontend/.env.local`
|
|
23
|
+
3. `tracing/.env`
|
|
24
|
+
4. `tracing/.env.local`
|
|
25
|
+
5. `tracing/examples/.env`
|
|
26
|
+
6. `tracing/examples/.env.local`
|
|
27
|
+
|
|
28
|
+
That means if your provider keys already live in `rl/frontend/.env`, the
|
|
29
|
+
examples can usually use them without any extra setup.
|
|
30
|
+
|
|
31
|
+
## Run
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd tracing
|
|
35
|
+
uv run python -m examples.openai.chat "Write a two-sentence launch blurb."
|
|
36
|
+
uv run python -m examples.openai.vision https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg --prompt "Describe the image in three bullet points."
|
|
37
|
+
uv run python -m examples.anthropic.chat "Summarize why tracing is useful."
|
|
38
|
+
uv run python -m examples.anthropic.vision https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg --prompt "What is in this image?"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For the vision examples you can pass either:
|
|
42
|
+
|
|
43
|
+
- an `https://...` image URL
|
|
44
|
+
- a local `.png`, `.jpg`, `.jpeg`, `.gif`, or `.webp` path
|
|
45
|
+
|
|
46
|
+
If `FREESOLO_BASE_URL` and `FREESOLO_API_KEY` are missing, the
|
|
47
|
+
provider calls still run but trace delivery is disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runnable examples for the freesolo package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Anthropic example scripts."""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from anthropic import Anthropic
|
|
6
|
+
|
|
7
|
+
from freesolo import instrument_anthropic, start_trace, trace
|
|
8
|
+
|
|
9
|
+
from ..utils import (
|
|
10
|
+
configure_example,
|
|
11
|
+
extract_anthropic_text,
|
|
12
|
+
print_runtime_status,
|
|
13
|
+
require_env,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
CHAT_MODEL = "claude-sonnet-4-20250514"
|
|
17
|
+
|
|
18
|
+
client: Anthropic | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_args() -> argparse.Namespace:
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="Anthropic chat example with tracing."
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"prompt",
|
|
27
|
+
nargs="?",
|
|
28
|
+
default="Summarize why tracing is useful for LLM applications.",
|
|
29
|
+
)
|
|
30
|
+
return parser.parse_args()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_client() -> Anthropic:
|
|
34
|
+
if client is None:
|
|
35
|
+
raise RuntimeError("Anthropic client has not been initialized.")
|
|
36
|
+
return client
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@trace()
|
|
40
|
+
def run_chat(prompt: str) -> str:
|
|
41
|
+
response = get_client().messages.create(
|
|
42
|
+
model=CHAT_MODEL,
|
|
43
|
+
max_tokens=300,
|
|
44
|
+
system="You are a concise assistant. Reply in plain text.",
|
|
45
|
+
messages=[
|
|
46
|
+
{
|
|
47
|
+
"role": "user",
|
|
48
|
+
"content": prompt,
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
return extract_anthropic_text(response)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> None:
|
|
56
|
+
global client
|
|
57
|
+
|
|
58
|
+
args = parse_args()
|
|
59
|
+
configure_example()
|
|
60
|
+
|
|
61
|
+
client = instrument_anthropic(
|
|
62
|
+
Anthropic(api_key=require_env("ANTHROPIC_API_KEY"))
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
print_runtime_status(provider="anthropic", model=CHAT_MODEL)
|
|
66
|
+
print()
|
|
67
|
+
with start_trace("example_anthropic_chat"):
|
|
68
|
+
print(run_chat(args.prompt))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from anthropic import Anthropic
|
|
6
|
+
|
|
7
|
+
from freesolo import instrument_anthropic, start_trace, trace
|
|
8
|
+
|
|
9
|
+
from ..utils import (
|
|
10
|
+
configure_example,
|
|
11
|
+
extract_anthropic_text,
|
|
12
|
+
load_image_input,
|
|
13
|
+
print_runtime_status,
|
|
14
|
+
require_env,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
VISION_MODEL = "claude-sonnet-4-20250514"
|
|
18
|
+
|
|
19
|
+
client: Anthropic | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_args() -> argparse.Namespace:
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
description="Anthropic vision example with tracing."
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"image",
|
|
28
|
+
help="Local image path or https URL.",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--prompt",
|
|
32
|
+
default="What is in this image? Keep the answer concise.",
|
|
33
|
+
)
|
|
34
|
+
return parser.parse_args()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_client() -> Anthropic:
|
|
38
|
+
if client is None:
|
|
39
|
+
raise RuntimeError("Anthropic client has not been initialized.")
|
|
40
|
+
return client
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@trace()
|
|
44
|
+
def run_vision(prompt: str, image_reference: str) -> str:
|
|
45
|
+
image = load_image_input(image_reference)
|
|
46
|
+
if image["kind"] == "url":
|
|
47
|
+
image_block = {
|
|
48
|
+
"type": "image",
|
|
49
|
+
"source": {
|
|
50
|
+
"type": "url",
|
|
51
|
+
"url": image["url"],
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
else:
|
|
55
|
+
image_block = {
|
|
56
|
+
"type": "image",
|
|
57
|
+
"source": {
|
|
58
|
+
"type": "base64",
|
|
59
|
+
"media_type": image["media_type"],
|
|
60
|
+
"data": image["base64_data"],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
response = get_client().messages.create(
|
|
65
|
+
model=VISION_MODEL,
|
|
66
|
+
max_tokens=400,
|
|
67
|
+
messages=[
|
|
68
|
+
{
|
|
69
|
+
"role": "user",
|
|
70
|
+
"content": [
|
|
71
|
+
image_block,
|
|
72
|
+
{"type": "text", "text": prompt},
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
return extract_anthropic_text(response)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main() -> None:
|
|
81
|
+
global client
|
|
82
|
+
|
|
83
|
+
args = parse_args()
|
|
84
|
+
configure_example()
|
|
85
|
+
|
|
86
|
+
client = instrument_anthropic(
|
|
87
|
+
Anthropic(api_key=require_env("ANTHROPIC_API_KEY"))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
print_runtime_status(provider="anthropic", model=VISION_MODEL)
|
|
91
|
+
print()
|
|
92
|
+
with start_trace("example_anthropic_vision"):
|
|
93
|
+
print(run_vision(args.prompt, args.image))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OpenAI example scripts."""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
|
|
7
|
+
from freesolo import instrument_openai, start_trace, trace
|
|
8
|
+
|
|
9
|
+
from ..utils import (
|
|
10
|
+
configure_example,
|
|
11
|
+
extract_openai_text,
|
|
12
|
+
print_runtime_status,
|
|
13
|
+
require_env,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
CHAT_MODEL = "gpt-4.1-mini"
|
|
17
|
+
|
|
18
|
+
client: OpenAI | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_args() -> argparse.Namespace:
|
|
22
|
+
parser = argparse.ArgumentParser(description="OpenAI chat example with tracing.")
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"prompt",
|
|
25
|
+
nargs="?",
|
|
26
|
+
default="Write a two-sentence launch blurb for Reinforcement Labs.",
|
|
27
|
+
)
|
|
28
|
+
return parser.parse_args()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_client() -> OpenAI:
|
|
32
|
+
if client is None:
|
|
33
|
+
raise RuntimeError("OpenAI client has not been initialized.")
|
|
34
|
+
return client
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@trace()
|
|
38
|
+
def run_chat(prompt: str) -> str:
|
|
39
|
+
response = get_client().chat.completions.create(
|
|
40
|
+
model=CHAT_MODEL,
|
|
41
|
+
messages=[
|
|
42
|
+
{
|
|
43
|
+
"role": "system",
|
|
44
|
+
"content": "You are a concise assistant. Reply in plain text.",
|
|
45
|
+
},
|
|
46
|
+
{"role": "user", "content": prompt},
|
|
47
|
+
],
|
|
48
|
+
max_tokens=300,
|
|
49
|
+
)
|
|
50
|
+
return extract_openai_text(response)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> None:
|
|
54
|
+
global client
|
|
55
|
+
|
|
56
|
+
args = parse_args()
|
|
57
|
+
configure_example()
|
|
58
|
+
|
|
59
|
+
client = instrument_openai(OpenAI(api_key=require_env("OPENAI_API_KEY")))
|
|
60
|
+
|
|
61
|
+
print_runtime_status(provider="openai", model=CHAT_MODEL)
|
|
62
|
+
print()
|
|
63
|
+
with start_trace("example_openai_chat"):
|
|
64
|
+
print(run_chat(args.prompt))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
|
|
7
|
+
from freesolo import instrument_openai, start_trace, trace
|
|
8
|
+
|
|
9
|
+
from ..utils import (
|
|
10
|
+
configure_example,
|
|
11
|
+
extract_openai_text,
|
|
12
|
+
load_image_input,
|
|
13
|
+
print_runtime_status,
|
|
14
|
+
require_env,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
VISION_MODEL = "gpt-4.1-mini"
|
|
18
|
+
|
|
19
|
+
client: OpenAI | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_args() -> argparse.Namespace:
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
description="OpenAI vision example with tracing."
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"image",
|
|
28
|
+
help="Local image path or https URL.",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--prompt",
|
|
32
|
+
default="Describe this image in three bullet points.",
|
|
33
|
+
)
|
|
34
|
+
return parser.parse_args()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_client() -> OpenAI:
|
|
38
|
+
if client is None:
|
|
39
|
+
raise RuntimeError("OpenAI client has not been initialized.")
|
|
40
|
+
return client
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@trace()
|
|
44
|
+
def run_vision(prompt: str, image_reference: str) -> str:
|
|
45
|
+
image = load_image_input(image_reference)
|
|
46
|
+
image_url = image["url"] if image["kind"] == "url" else image["data_url"]
|
|
47
|
+
|
|
48
|
+
response = get_client().chat.completions.create(
|
|
49
|
+
model=VISION_MODEL,
|
|
50
|
+
messages=[
|
|
51
|
+
{
|
|
52
|
+
"role": "user",
|
|
53
|
+
"content": [
|
|
54
|
+
{"type": "text", "text": prompt},
|
|
55
|
+
{"type": "image_url", "image_url": {"url": image_url}},
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
max_tokens=400,
|
|
60
|
+
)
|
|
61
|
+
return extract_openai_text(response)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
global client
|
|
66
|
+
|
|
67
|
+
args = parse_args()
|
|
68
|
+
configure_example()
|
|
69
|
+
|
|
70
|
+
client = instrument_openai(OpenAI(api_key=require_env("OPENAI_API_KEY")))
|
|
71
|
+
|
|
72
|
+
print_runtime_status(provider="openai", model=VISION_MODEL)
|
|
73
|
+
print()
|
|
74
|
+
with start_trace("example_openai_vision"):
|
|
75
|
+
print(run_vision(args.prompt, args.image))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|