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.
@@ -0,0 +1,2 @@
1
+ FREESOLO_BASE_URL=http://localhost:3000
2
+ FREESOLO_API_KEY=
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ node_modules/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ *.pyc
6
+ .env
7
+ .env.local
8
+ examples/.env
9
+ examples/.env.local
@@ -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.
@@ -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,6 @@
1
+ OPENAI_API_KEY=
2
+
3
+ ANTHROPIC_API_KEY=
4
+
5
+ FREESOLO_BASE_URL=http://localhost:3000
6
+ FREESOLO_API_KEY=
@@ -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()