use-lightcurve 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.
- use_lightcurve-0.1.0/PKG-INFO +46 -0
- use_lightcurve-0.1.0/README.md +22 -0
- use_lightcurve-0.1.0/lightcurve/__init__.py +3 -0
- use_lightcurve-0.1.0/lightcurve/buffer.py +81 -0
- use_lightcurve-0.1.0/lightcurve/client.py +75 -0
- use_lightcurve-0.1.0/lightcurve/context.py +9 -0
- use_lightcurve-0.1.0/lightcurve/schemas.py +32 -0
- use_lightcurve-0.1.0/lightcurve/tracer.py +160 -0
- use_lightcurve-0.1.0/lightcurve/transport.py +53 -0
- use_lightcurve-0.1.0/setup.cfg +4 -0
- use_lightcurve-0.1.0/setup.py +23 -0
- use_lightcurve-0.1.0/use_lightcurve.egg-info/PKG-INFO +46 -0
- use_lightcurve-0.1.0/use_lightcurve.egg-info/SOURCES.txt +14 -0
- use_lightcurve-0.1.0/use_lightcurve.egg-info/dependency_links.txt +1 -0
- use_lightcurve-0.1.0/use_lightcurve.egg-info/requires.txt +2 -0
- use_lightcurve-0.1.0/use_lightcurve.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: use-lightcurve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Observability and evaluation SDK for LLM Agents
|
|
5
|
+
Home-page: https://github.com/uselightcurve/lightcurve-sdk
|
|
6
|
+
Author: Lightcurve Team
|
|
7
|
+
Author-email: founders@lightcurve.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests>=2.25.0
|
|
14
|
+
Requires-Dist: python-dotenv>=0.10.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# use-lightcurve
|
|
26
|
+
|
|
27
|
+
The official Python SDK for Lightcurve, the observability and evaluation platform for LLM Agents.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install use-lightcurve
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from lightcurve import Lightcurve
|
|
39
|
+
|
|
40
|
+
lc = Lightcurve(api_key="your_api_key")
|
|
41
|
+
|
|
42
|
+
# Track a run
|
|
43
|
+
with lc.trace(agent_id="my-agent") as run:
|
|
44
|
+
result = my_agent.run("input prompt")
|
|
45
|
+
run.log_output(result)
|
|
46
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# use-lightcurve
|
|
2
|
+
|
|
3
|
+
The official Python SDK for Lightcurve, the observability and evaluation platform for LLM Agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install use-lightcurve
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from lightcurve import Lightcurve
|
|
15
|
+
|
|
16
|
+
lc = Lightcurve(api_key="your_api_key")
|
|
17
|
+
|
|
18
|
+
# Track a run
|
|
19
|
+
with lc.trace(agent_id="my-agent") as run:
|
|
20
|
+
result = my_agent.run("input prompt")
|
|
21
|
+
run.log_output(result)
|
|
22
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import queue
|
|
3
|
+
import time
|
|
4
|
+
import requests # using sync requests in thread
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from .schemas import CognitionEvent
|
|
7
|
+
|
|
8
|
+
class EventBuffer:
|
|
9
|
+
def __init__(self, api_url: str, api_key: Optional[str] = None, batch_size: int = 10, flush_interval: float = 2.0):
|
|
10
|
+
self.api_url = api_url
|
|
11
|
+
self.api_key = api_key
|
|
12
|
+
self.batch_size = batch_size
|
|
13
|
+
self.flush_interval = flush_interval
|
|
14
|
+
|
|
15
|
+
self._queue: queue.Queue[CognitionEvent] = queue.Queue()
|
|
16
|
+
self._stop_event = threading.Event()
|
|
17
|
+
self._worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
|
|
18
|
+
self._worker_thread.start()
|
|
19
|
+
|
|
20
|
+
def add_event(self, event: CognitionEvent):
|
|
21
|
+
self._queue.put(event)
|
|
22
|
+
|
|
23
|
+
def flush(self):
|
|
24
|
+
"""Force flush all events in queue."""
|
|
25
|
+
# Simple implementation: wait for queue to be empty
|
|
26
|
+
# In a real system we might pause specific ingestion to flush.
|
|
27
|
+
# Here we just wait a bit or could trigger immediate send.
|
|
28
|
+
self._queue.join()
|
|
29
|
+
|
|
30
|
+
def close(self):
|
|
31
|
+
self._stop_event.set()
|
|
32
|
+
self._worker_thread.join(timeout=5.0)
|
|
33
|
+
|
|
34
|
+
def _worker_loop(self):
|
|
35
|
+
batch = []
|
|
36
|
+
last_flush = time.time()
|
|
37
|
+
|
|
38
|
+
while not self._stop_event.is_set():
|
|
39
|
+
try:
|
|
40
|
+
# Wait for items nicely
|
|
41
|
+
try:
|
|
42
|
+
event = self._queue.get(timeout=0.5)
|
|
43
|
+
batch.append(event)
|
|
44
|
+
except queue.Empty:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
current_time = time.time()
|
|
48
|
+
is_batch_full = len(batch) >= self.batch_size
|
|
49
|
+
is_time_to_flush = (current_time - last_flush) >= self.flush_interval and len(batch) > 0
|
|
50
|
+
|
|
51
|
+
if is_batch_full or is_time_to_flush:
|
|
52
|
+
self._send_batch(batch)
|
|
53
|
+
# Mark tasks as done in queue
|
|
54
|
+
for _ in range(len(batch)):
|
|
55
|
+
self._queue.task_done()
|
|
56
|
+
batch = []
|
|
57
|
+
last_flush = current_time
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"[Lightcurve] Error in buffer worker: {e}")
|
|
61
|
+
|
|
62
|
+
# Final flush on exit
|
|
63
|
+
if batch:
|
|
64
|
+
self._send_batch(batch)
|
|
65
|
+
|
|
66
|
+
def _send_batch(self, batch: List[CognitionEvent]):
|
|
67
|
+
try:
|
|
68
|
+
payload = {
|
|
69
|
+
"events": [event.model_dump(mode='json') for event in batch]
|
|
70
|
+
}
|
|
71
|
+
# Assuming api_url is the base url, e.g., http://localhost:8000
|
|
72
|
+
endpoint = f"{self.api_url}/v1/ingest"
|
|
73
|
+
headers = {"Content-Type": "application/json"}
|
|
74
|
+
if self.api_key:
|
|
75
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
76
|
+
|
|
77
|
+
response = requests.post(endpoint, json=payload, headers=headers, timeout=10)
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
except Exception as e:
|
|
80
|
+
# In a real system, we would retry or retry-backoff
|
|
81
|
+
print(f"[Lightcurve] Failed to send batch: {e}")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import List, Optional, Dict, Any
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from .schemas import (
|
|
6
|
+
CognitionEvent, InputPayload, KnowledgePayload, PlanPayload,
|
|
7
|
+
ToolPayload, ValidationPayload, OutputPayload, IncidentFlags
|
|
8
|
+
)
|
|
9
|
+
from .buffer import EventBuffer
|
|
10
|
+
|
|
11
|
+
class Lightcurve:
|
|
12
|
+
def __init__(self, api_key: Optional[str] = None, base_url: str = "http://localhost:8000"):
|
|
13
|
+
self.api_key = api_key
|
|
14
|
+
self.base_url = base_url
|
|
15
|
+
self.buffer = EventBuffer(api_url=base_url, api_key=api_key)
|
|
16
|
+
|
|
17
|
+
def start_run(self, agent_id: str, run_id: Optional[str] = None, org_id: Optional[str] = None) -> 'Run':
|
|
18
|
+
if not run_id:
|
|
19
|
+
run_id = str(uuid.uuid4())
|
|
20
|
+
return Run(client=self, run_id=run_id, agent_id=agent_id, org_id=org_id)
|
|
21
|
+
|
|
22
|
+
def close(self):
|
|
23
|
+
self.buffer.close()
|
|
24
|
+
|
|
25
|
+
class Run:
|
|
26
|
+
def __init__(self, client: Lightcurve, run_id: str, agent_id: str, org_id: Optional[str] = None):
|
|
27
|
+
self.client = client
|
|
28
|
+
self.run_id = run_id
|
|
29
|
+
self.agent_id = agent_id
|
|
30
|
+
self.org_id = org_id
|
|
31
|
+
|
|
32
|
+
def _log(self, type: str, data: Any, incident_flags: Optional[IncidentFlags] = None):
|
|
33
|
+
event = CognitionEvent(
|
|
34
|
+
run_id=self.run_id,
|
|
35
|
+
agent_id=self.agent_id,
|
|
36
|
+
org_id=self.org_id,
|
|
37
|
+
timestamp=datetime.now(timezone.utc),
|
|
38
|
+
type=type,
|
|
39
|
+
data=data,
|
|
40
|
+
incident_flags=incident_flags
|
|
41
|
+
)
|
|
42
|
+
self.client.buffer.add_event(event)
|
|
43
|
+
|
|
44
|
+
def log_input(self, user_input: str, interpreted_goal: Optional[str] = None, constraints: List[str] = []):
|
|
45
|
+
data = InputPayload(user_input=user_input, interpreted_goal=interpreted_goal, constraints=constraints)
|
|
46
|
+
self._log("input", data)
|
|
47
|
+
|
|
48
|
+
def log_knowledge(self, summary: str, evidence_present: bool = False, sources: List[str] = []):
|
|
49
|
+
data = KnowledgePayload(summary=summary, evidence_present=evidence_present, sources=sources)
|
|
50
|
+
self._log("knowledge", data)
|
|
51
|
+
|
|
52
|
+
def log_plan(self, steps: List[str], rationale: Optional[str] = None, alternatives: List[str] = []):
|
|
53
|
+
data = PlanPayload(steps=steps, rationale=rationale, alternatives=alternatives)
|
|
54
|
+
self._log("plan", data)
|
|
55
|
+
|
|
56
|
+
def log_tool(self, tool_name: str, input: Dict[str, Any], output: Optional[Dict[str, Any]] = None,
|
|
57
|
+
success: bool = True, retries: int = 0, latency_ms: Optional[float] = None):
|
|
58
|
+
data = ToolPayload(
|
|
59
|
+
tool_name=tool_name, input=input, output=output,
|
|
60
|
+
success=success, retries=retries, latency_ms=latency_ms
|
|
61
|
+
)
|
|
62
|
+
self._log("tool", data)
|
|
63
|
+
|
|
64
|
+
def log_validation(self, result: bool, method: str = "auto", confidence_score: Optional[float] = None):
|
|
65
|
+
data = ValidationPayload(result=result, method=method, confidence_score=confidence_score)
|
|
66
|
+
self._log("validation", data)
|
|
67
|
+
|
|
68
|
+
def log_output(self, content: str, structured_data: Optional[Dict[str, Any]] = None,
|
|
69
|
+
confidence: Optional[float] = None):
|
|
70
|
+
data = OutputPayload(content=content, structured_data=structured_data, confidence=confidence)
|
|
71
|
+
self._log("output", data)
|
|
72
|
+
|
|
73
|
+
def end(self):
|
|
74
|
+
# Flush the buffer to ensure all events are sent
|
|
75
|
+
self.client.buffer.flush()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class active_run_context:
|
|
7
|
+
steps: List[Dict[str, Any]] = field(default_factory=list)
|
|
8
|
+
|
|
9
|
+
run_ctx = ContextVar("lightcurve_run_ctx", default=None)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Optional, Dict, Any, Literal
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
class StepPayload(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
type: str # "tool", "llm", "planning", "custom"
|
|
9
|
+
stage: Optional[str] = None # Mapped to backend stage
|
|
10
|
+
cognitive_type: Optional[str] = None # Cognitive phase
|
|
11
|
+
status: str # "success", "failure"
|
|
12
|
+
input: Optional[Dict[str, Any]] = None
|
|
13
|
+
output: Optional[Dict[str, Any]] = None
|
|
14
|
+
content: Optional[Dict[str, Any]] = None # Generic content container for persistence
|
|
15
|
+
duration_ms: Optional[float] = None
|
|
16
|
+
started_at: Optional[datetime] = None
|
|
17
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
18
|
+
|
|
19
|
+
class RunPayload(BaseModel):
|
|
20
|
+
run_id: str
|
|
21
|
+
name: str
|
|
22
|
+
org_id: str = "default-org" # Default for single-tenant / local
|
|
23
|
+
agent_id: str # Mapped from name if not provided
|
|
24
|
+
project: Optional[str] = None
|
|
25
|
+
input: Optional[Dict[str, Any]] = None
|
|
26
|
+
output: Optional[Dict[str, Any]] = None
|
|
27
|
+
status: str # "success", "failure"
|
|
28
|
+
duration_ms: Optional[float] = None
|
|
29
|
+
started_at: datetime
|
|
30
|
+
steps: List[StepPayload] = Field(default_factory=list)
|
|
31
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
32
|
+
exception: Optional[str] = None
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
import time
|
|
4
|
+
import functools
|
|
5
|
+
import datetime
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from typing import Optional, Dict, Any, List, Union
|
|
8
|
+
from .transport import BackgroundTransport
|
|
9
|
+
from .schemas import RunPayload, StepPayload
|
|
10
|
+
from .context import run_ctx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
COGNITIVE_PHASES = {
|
|
14
|
+
"planning",
|
|
15
|
+
"goal_decomposition",
|
|
16
|
+
"reflection",
|
|
17
|
+
"self_critique",
|
|
18
|
+
"tool_selection",
|
|
19
|
+
"validation",
|
|
20
|
+
"synthesis"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class GlobalTracer:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.transport: Optional[BackgroundTransport] = None
|
|
26
|
+
self._api_key: Optional[str] = None
|
|
27
|
+
self.project: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
def init(self, api_key: Optional[str] = None, base_url: str = "http://localhost:8000"):
|
|
30
|
+
self._api_key = api_key or os.getenv("LIGHTCURVE_API_KEY")
|
|
31
|
+
self.transport = BackgroundTransport(api_key=self._api_key, api_url=base_url)
|
|
32
|
+
|
|
33
|
+
def trace(self, name: Optional[str] = None, project: Optional[str] = None, tags: Optional[Dict[str, Any]] = None):
|
|
34
|
+
def decorator(func):
|
|
35
|
+
@functools.wraps(func)
|
|
36
|
+
def wrapper(*args, **kwargs):
|
|
37
|
+
if not self.transport:
|
|
38
|
+
# If not inited, just run the function
|
|
39
|
+
return func(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
status = "success"
|
|
42
|
+
error_msg = None
|
|
43
|
+
output = None
|
|
44
|
+
|
|
45
|
+
# Start Run
|
|
46
|
+
start_time = time.time()
|
|
47
|
+
run_id = str(uuid.uuid4())
|
|
48
|
+
|
|
49
|
+
# Set Context
|
|
50
|
+
token = run_ctx.set([]) # Initialize steps list
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = func(*args, **kwargs)
|
|
54
|
+
output = {"return_value": str(result)} # Simplistic stringification
|
|
55
|
+
return result
|
|
56
|
+
except Exception as e:
|
|
57
|
+
status = "failure"
|
|
58
|
+
error_msg = str(e)
|
|
59
|
+
output = {"error": error_msg}
|
|
60
|
+
raise e
|
|
61
|
+
finally:
|
|
62
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
63
|
+
steps = run_ctx.get()
|
|
64
|
+
run_ctx.reset(token)
|
|
65
|
+
|
|
66
|
+
# Build Payload
|
|
67
|
+
payload = RunPayload(
|
|
68
|
+
run_id=run_id,
|
|
69
|
+
name=name or func.__name__,
|
|
70
|
+
agent_id=name or func.__name__, # Trace name = agent_id for now
|
|
71
|
+
org_id="default-org",
|
|
72
|
+
project=project or self.project,
|
|
73
|
+
started_at=datetime.datetime.now(datetime.timezone.utc),
|
|
74
|
+
duration_ms=duration_ms,
|
|
75
|
+
status=status,
|
|
76
|
+
input={"args": str(args), "kwargs": str(kwargs)}, # Redact in prod
|
|
77
|
+
output=output,
|
|
78
|
+
steps=steps, # now this is List[StepPayload]
|
|
79
|
+
metadata=tags,
|
|
80
|
+
exception=error_msg
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Send
|
|
84
|
+
self.transport.send(payload)
|
|
85
|
+
|
|
86
|
+
return wrapper
|
|
87
|
+
return decorator
|
|
88
|
+
|
|
89
|
+
def step(self, name: str, type: str = "custom", metadata: Optional[Dict[str, Any]] = None):
|
|
90
|
+
return StepContext(name, type, metadata)
|
|
91
|
+
|
|
92
|
+
def cognitive(self, phase: str, name: Optional[str] = None):
|
|
93
|
+
if phase not in COGNITIVE_PHASES:
|
|
94
|
+
# Soft warning
|
|
95
|
+
print(f"Warning: Unknown cognitive phase '{phase}'. Expected one of {COGNITIVE_PHASES}")
|
|
96
|
+
return StepContext(name or phase, "cognitive", {"cognitive_phase": phase})
|
|
97
|
+
|
|
98
|
+
class StepContext:
|
|
99
|
+
def __init__(self, name: str, type: str, metadata: Optional[Dict[str, Any]]):
|
|
100
|
+
self.name = name
|
|
101
|
+
self.type = type
|
|
102
|
+
self.metadata = metadata or {}
|
|
103
|
+
self.start_time = None
|
|
104
|
+
self.cognitive_type = None
|
|
105
|
+
|
|
106
|
+
if type == "cognitive":
|
|
107
|
+
self.cognitive_type = self.metadata.get("cognitive_phase")
|
|
108
|
+
|
|
109
|
+
def __enter__(self):
|
|
110
|
+
self.start_time = time.time()
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
114
|
+
duration = (time.time() - self.start_time) * 1000
|
|
115
|
+
steps = run_ctx.get()
|
|
116
|
+
|
|
117
|
+
# Map SDK type to Backend Stage
|
|
118
|
+
stage_map = {
|
|
119
|
+
"llm": "execution_strategy",
|
|
120
|
+
"planning": "task_specification",
|
|
121
|
+
"custom": "execution_strategy",
|
|
122
|
+
"cognitive": "execution_strategy" # Default mapping for cognitive steps if not overridden
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# If it's a cognitive step, map based on phase if needed, or keep generic stage
|
|
126
|
+
# For V1, let's map cognitive phases to stages roughly
|
|
127
|
+
if self.cognitive_type:
|
|
128
|
+
stage_map_cognitive = {
|
|
129
|
+
"planning": "task_specification",
|
|
130
|
+
"goal_decomposition": "task_specification",
|
|
131
|
+
"reflection": "validation",
|
|
132
|
+
"self_critique": "validation",
|
|
133
|
+
"validation": "validation",
|
|
134
|
+
"tool_selection": "tool_call",
|
|
135
|
+
"synthesis": "output"
|
|
136
|
+
}
|
|
137
|
+
stage = stage_map_cognitive.get(self.cognitive_type, "execution_strategy")
|
|
138
|
+
else:
|
|
139
|
+
stage = stage_map.get(self.type, "execution_strategy")
|
|
140
|
+
|
|
141
|
+
if steps is not None:
|
|
142
|
+
steps.append(StepPayload(
|
|
143
|
+
name=self.name,
|
|
144
|
+
type=self.type,
|
|
145
|
+
stage=stage,
|
|
146
|
+
cognitive_type=self.cognitive_type,
|
|
147
|
+
status="failure" if exc_type else "success",
|
|
148
|
+
started_at=datetime.datetime.now(datetime.timezone.utc),
|
|
149
|
+
duration_ms=duration,
|
|
150
|
+
metadata=self.metadata,
|
|
151
|
+
input={}, # Optionally capture
|
|
152
|
+
output={"error": str(exc_val)} if exc_val else {},
|
|
153
|
+
content={
|
|
154
|
+
"input": {},
|
|
155
|
+
"output": {"error": str(exc_val)} if exc_val else {}
|
|
156
|
+
}
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
# Global Singleton
|
|
160
|
+
tracer = GlobalTracer()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import queue
|
|
3
|
+
import time
|
|
4
|
+
import requests
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from .schemas import RunPayload
|
|
7
|
+
|
|
8
|
+
class BackgroundTransport:
|
|
9
|
+
def __init__(self, api_key: Optional[str], api_url: str = "http://localhost:8000"):
|
|
10
|
+
self.api_key = api_key
|
|
11
|
+
self.api_url = api_url.rstrip("/")
|
|
12
|
+
self._queue: queue.Queue[RunPayload] = queue.Queue()
|
|
13
|
+
self._stop_event = threading.Event()
|
|
14
|
+
self._thread = threading.Thread(target=self._worker, daemon=True)
|
|
15
|
+
self._thread.start()
|
|
16
|
+
|
|
17
|
+
def send(self, payload: RunPayload):
|
|
18
|
+
self._queue.put(payload)
|
|
19
|
+
|
|
20
|
+
def close(self):
|
|
21
|
+
self._stop_event.set()
|
|
22
|
+
self._thread.join(timeout=2.0)
|
|
23
|
+
|
|
24
|
+
def _worker(self):
|
|
25
|
+
while not self._stop_event.is_set():
|
|
26
|
+
try:
|
|
27
|
+
payload = self._queue.get(timeout=1.0)
|
|
28
|
+
self._post_payload(payload)
|
|
29
|
+
self._queue.task_done()
|
|
30
|
+
except queue.Empty:
|
|
31
|
+
continue
|
|
32
|
+
except Exception as e:
|
|
33
|
+
# In production, we should log this properly or use a dead-letter queue
|
|
34
|
+
# For now, we print to stderr if it's critical, or silent in production
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def _post_payload(self, payload: RunPayload):
|
|
38
|
+
# Retry logic could go here (3x exponential backoff)
|
|
39
|
+
url = f"{self.api_url}/ingest/trajectory"
|
|
40
|
+
headers = {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"x-api-key": self.api_key or ""
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# We dump to json using Pydantic
|
|
47
|
+
data = payload.model_dump(mode='json')
|
|
48
|
+
resp = requests.post(url, json=data, headers=headers, timeout=5)
|
|
49
|
+
if resp.status_code >= 400:
|
|
50
|
+
print(f"Error sending trace: {resp.status_code} {resp.text}")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"Network error sending trace: {e}")
|
|
53
|
+
pass
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="use-lightcurve",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
author="Lightcurve Team",
|
|
7
|
+
author_email="founders@lightcurve.ai",
|
|
8
|
+
description="Observability and evaluation SDK for LLM Agents",
|
|
9
|
+
long_description=open("README.md").read(),
|
|
10
|
+
long_description_content_type="text/markdown",
|
|
11
|
+
url="https://github.com/uselightcurve/lightcurve-sdk",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
classifiers=[
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
],
|
|
18
|
+
python_requires='>=3.7',
|
|
19
|
+
install_requires=[
|
|
20
|
+
"requests>=2.25.0",
|
|
21
|
+
"python-dotenv>=0.10.0"
|
|
22
|
+
],
|
|
23
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: use-lightcurve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Observability and evaluation SDK for LLM Agents
|
|
5
|
+
Home-page: https://github.com/uselightcurve/lightcurve-sdk
|
|
6
|
+
Author: Lightcurve Team
|
|
7
|
+
Author-email: founders@lightcurve.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests>=2.25.0
|
|
14
|
+
Requires-Dist: python-dotenv>=0.10.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# use-lightcurve
|
|
26
|
+
|
|
27
|
+
The official Python SDK for Lightcurve, the observability and evaluation platform for LLM Agents.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install use-lightcurve
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from lightcurve import Lightcurve
|
|
39
|
+
|
|
40
|
+
lc = Lightcurve(api_key="your_api_key")
|
|
41
|
+
|
|
42
|
+
# Track a run
|
|
43
|
+
with lc.trace(agent_id="my-agent") as run:
|
|
44
|
+
result = my_agent.run("input prompt")
|
|
45
|
+
run.log_output(result)
|
|
46
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
lightcurve/__init__.py
|
|
4
|
+
lightcurve/buffer.py
|
|
5
|
+
lightcurve/client.py
|
|
6
|
+
lightcurve/context.py
|
|
7
|
+
lightcurve/schemas.py
|
|
8
|
+
lightcurve/tracer.py
|
|
9
|
+
lightcurve/transport.py
|
|
10
|
+
use_lightcurve.egg-info/PKG-INFO
|
|
11
|
+
use_lightcurve.egg-info/SOURCES.txt
|
|
12
|
+
use_lightcurve.egg-info/dependency_links.txt
|
|
13
|
+
use_lightcurve.egg-info/requires.txt
|
|
14
|
+
use_lightcurve.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lightcurve
|