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.
@@ -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,3 @@
1
+ from .tracer import tracer
2
+
3
+ __all__ = ["tracer"]
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ requests>=2.25.0
2
+ python-dotenv>=0.10.0
@@ -0,0 +1 @@
1
+ lightcurve