fluxloop 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of fluxloop might be problematic. Click here for more details.

@@ -0,0 +1,197 @@
1
+ """
2
+ Trace and Observation models based on Langfuse data model.
3
+
4
+ References:
5
+ - https://langfuse.com/docs/observability/data-model
6
+ """
7
+
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ from typing import Any, Dict, List, Optional, Union
11
+ from uuid import UUID, uuid4
12
+
13
+ from pydantic import BaseModel, Field, field_validator
14
+
15
+
16
+ class TraceStatus(str, Enum):
17
+ """Status of a trace execution."""
18
+
19
+ PENDING = "pending"
20
+ RUNNING = "running"
21
+ SUCCESS = "success"
22
+ FAILED = "failed"
23
+ CANCELLED = "cancelled"
24
+
25
+
26
+ class ObservationType(str, Enum):
27
+ """Type of observation in the trace hierarchy."""
28
+
29
+ SPAN = "span" # Generic span
30
+ EVENT = "event" # Single event
31
+ GENERATION = "generation" # LLM generation
32
+ TOOL = "tool" # Tool/function call
33
+ AGENT = "agent" # Agent execution
34
+ CHAIN = "chain" # Chain of operations
35
+ EVALUATION = "evaluation" # Evaluation result
36
+
37
+
38
+ class ObservationLevel(str, Enum):
39
+ """Log level for observations."""
40
+
41
+ DEBUG = "debug"
42
+ INFO = "info"
43
+ WARNING = "warning"
44
+ ERROR = "error"
45
+
46
+
47
+ class ScoreDataType(str, Enum):
48
+ """Data type of score values."""
49
+
50
+ NUMERIC = "numeric"
51
+ BOOLEAN = "boolean"
52
+ CATEGORICAL = "categorical"
53
+
54
+
55
+ class Score(BaseModel):
56
+ """Score/evaluation result for a trace or observation."""
57
+
58
+ id: UUID = Field(default_factory=uuid4)
59
+ trace_id: UUID
60
+ observation_id: Optional[UUID] = None
61
+ name: str # e.g., "accuracy", "relevance", "latency"
62
+ value: Union[float, bool, str]
63
+ data_type: ScoreDataType
64
+ comment: Optional[str] = None
65
+ metadata: Dict[str, Any] = Field(default_factory=dict)
66
+ created_at: datetime = Field(default_factory=datetime.utcnow)
67
+
68
+ @field_validator("value")
69
+ def validate_value_type(cls, v, info):
70
+ """Ensure value matches the declared data type."""
71
+ data_type = info.data.get("data_type")
72
+ if data_type == ScoreDataType.NUMERIC and not isinstance(v, (int, float)):
73
+ raise ValueError("Numeric score must be int or float")
74
+ elif data_type == ScoreDataType.BOOLEAN and not isinstance(v, bool):
75
+ raise ValueError("Boolean score must be bool")
76
+ elif data_type == ScoreDataType.CATEGORICAL and not isinstance(v, str):
77
+ raise ValueError("Categorical score must be str")
78
+ return v
79
+
80
+
81
+ class Observation(BaseModel):
82
+ """Single observation within a trace."""
83
+
84
+ id: UUID = Field(default_factory=uuid4)
85
+ trace_id: UUID
86
+ parent_observation_id: Optional[UUID] = None
87
+ type: ObservationType
88
+ name: str
89
+
90
+ # Timing
91
+ start_time: datetime = Field(default_factory=datetime.utcnow)
92
+ end_time: Optional[datetime] = None
93
+
94
+ # Content
95
+ input: Optional[Any] = None
96
+ output: Optional[Any] = None
97
+ error: Optional[str] = None
98
+
99
+ # Metadata
100
+ level: ObservationLevel = ObservationLevel.INFO
101
+ status_message: Optional[str] = None
102
+ metadata: Dict[str, Any] = Field(default_factory=dict)
103
+
104
+ # LLM specific (for GENERATION type)
105
+ model: Optional[str] = None
106
+ llm_parameters: Optional[Dict[str, Any]] = None
107
+ prompt_tokens: Optional[int] = None
108
+ completion_tokens: Optional[int] = None
109
+ total_tokens: Optional[int] = None
110
+
111
+ # Scores attached to this observation
112
+ scores: List[Score] = Field(default_factory=list)
113
+
114
+ def duration_ms(self) -> Optional[float]:
115
+ """Calculate duration in milliseconds."""
116
+ if self.start_time and self.end_time:
117
+ delta = self.end_time - self.start_time
118
+ return delta.total_seconds() * 1000
119
+ return None
120
+
121
+
122
+ class Trace(BaseModel):
123
+ """Root trace representing a complete execution flow."""
124
+
125
+ id: UUID = Field(default_factory=uuid4)
126
+ session_id: Optional[UUID] = None
127
+ name: str
128
+
129
+ # Timing
130
+ start_time: datetime = Field(default_factory=datetime.utcnow)
131
+ end_time: Optional[datetime] = None
132
+
133
+ # Status
134
+ status: TraceStatus = TraceStatus.PENDING
135
+ error: Optional[str] = None
136
+
137
+ # Context
138
+ user_id: Optional[str] = None
139
+ metadata: Dict[str, Any] = Field(default_factory=dict)
140
+ tags: List[str] = Field(default_factory=list)
141
+
142
+ # Experiment specific
143
+ experiment_id: Optional[str] = None
144
+ iteration: Optional[int] = None
145
+ persona: Optional[str] = None
146
+ variation_seed: Optional[str] = None
147
+
148
+ # Input/Output
149
+ input: Optional[Any] = None
150
+ output: Optional[Any] = None
151
+
152
+ # Hierarchical data
153
+ observations: List[Observation] = Field(default_factory=list)
154
+ scores: List[Score] = Field(default_factory=list)
155
+
156
+ # Metrics
157
+ total_cost: Optional[float] = None
158
+ prompt_tokens: Optional[int] = None
159
+ completion_tokens: Optional[int] = None
160
+ total_tokens: Optional[int] = None
161
+
162
+ def duration_ms(self) -> Optional[float]:
163
+ """Calculate total duration in milliseconds."""
164
+ if self.start_time and self.end_time:
165
+ delta = self.end_time - self.start_time
166
+ return delta.total_seconds() * 1000
167
+ return None
168
+
169
+ def get_observation_tree(self) -> Dict[UUID, List[Observation]]:
170
+ """Build parent-child relationship tree of observations."""
171
+ tree: Dict[UUID, List[Observation]] = {None: []}
172
+
173
+ for obs in self.observations:
174
+ parent_id = obs.parent_observation_id
175
+ if parent_id not in tree:
176
+ tree[parent_id] = []
177
+ tree[parent_id].append(obs)
178
+
179
+ return tree
180
+
181
+ def calculate_metrics(self) -> Dict[str, Any]:
182
+ """Calculate aggregate metrics for the trace."""
183
+ metrics = {
184
+ "duration_ms": self.duration_ms(),
185
+ "observation_count": len(self.observations),
186
+ "score_count": len(self.scores),
187
+ "error_count": sum(1 for o in self.observations if o.error),
188
+ "total_tokens": self.total_tokens or 0,
189
+ }
190
+
191
+ # Calculate success rate from scores
192
+ success_scores = [s for s in self.scores if s.name == "success" and s.data_type == ScoreDataType.BOOLEAN]
193
+ if success_scores:
194
+ success_rate = sum(1 for s in success_scores if s.value) / len(success_scores)
195
+ metrics["success_rate"] = success_rate
196
+
197
+ return metrics
@@ -0,0 +1,116 @@
1
+ """Utilities for serializing trace and observation data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from typing import Any, Dict
8
+ from uuid import UUID
9
+
10
+ from .models import ObservationData, TraceData
11
+
12
+
13
+ def _convert_uuid(value: UUID | None) -> str | None:
14
+ if value is None:
15
+ return None
16
+ return str(value)
17
+
18
+
19
+ def _convert_datetime(value: datetime | None) -> str | None:
20
+ if value is None:
21
+ return None
22
+ # Use ISO 8601 format with UTC indicator when possible
23
+ iso = value.isoformat()
24
+ if value.tzinfo is None:
25
+ return iso + "Z"
26
+ return iso
27
+
28
+
29
+ def _make_json_safe(value: Any) -> Any:
30
+ """Recursively convert values so they can be JSON-serialized."""
31
+
32
+ if isinstance(value, datetime):
33
+ return _convert_datetime(value)
34
+
35
+ if isinstance(value, UUID):
36
+ return str(value)
37
+
38
+ if isinstance(value, Enum):
39
+ return value.value
40
+
41
+ if isinstance(value, dict):
42
+ return {str(key): _make_json_safe(val) for key, val in value.items()}
43
+
44
+ if isinstance(value, (list, tuple, set, frozenset)):
45
+ return [
46
+ _make_json_safe(item)
47
+ for item in value
48
+ ]
49
+
50
+ if isinstance(value, bytes):
51
+ return value.decode("utf-8", errors="replace")
52
+
53
+ if isinstance(value, (str, int, float, bool)) or value is None:
54
+ return value
55
+
56
+ return repr(value)
57
+
58
+
59
+ def serialize_trace(trace: TraceData) -> Dict[str, Any]:
60
+ """Convert TraceData into a JSON-serializable dictionary."""
61
+
62
+ data = trace.model_dump(exclude_none=True)
63
+
64
+ if trace.id:
65
+ data["id"] = _convert_uuid(trace.id)
66
+ if trace.session_id:
67
+ data["session_id"] = _convert_uuid(trace.session_id)
68
+
69
+ if trace.start_time:
70
+ data["start_time"] = _convert_datetime(trace.start_time)
71
+ if trace.end_time:
72
+ data["end_time"] = _convert_datetime(trace.end_time)
73
+
74
+ if "metadata" in data:
75
+ data["metadata"] = _make_json_safe(data["metadata"])
76
+
77
+ if "input" in data:
78
+ data["input"] = _make_json_safe(data["input"])
79
+
80
+ if "output" in data:
81
+ data["output"] = _make_json_safe(data["output"])
82
+
83
+ return data
84
+
85
+
86
+ def serialize_observation(observation: ObservationData) -> Dict[str, Any]:
87
+ """Convert ObservationData into a JSON-serializable dictionary."""
88
+
89
+ data = observation.model_dump(exclude_none=True)
90
+
91
+ if observation.id:
92
+ data["id"] = _convert_uuid(observation.id)
93
+ if observation.trace_id:
94
+ data["trace_id"] = _convert_uuid(observation.trace_id)
95
+ if observation.parent_observation_id:
96
+ data["parent_observation_id"] = _convert_uuid(observation.parent_observation_id)
97
+
98
+ if observation.start_time:
99
+ data["start_time"] = _convert_datetime(observation.start_time)
100
+ if observation.end_time:
101
+ data["end_time"] = _convert_datetime(observation.end_time)
102
+
103
+ if "metadata" in data:
104
+ data["metadata"] = _make_json_safe(data["metadata"])
105
+
106
+ if "input" in data:
107
+ data["input"] = _make_json_safe(data["input"])
108
+
109
+ if "output" in data:
110
+ data["output"] = _make_json_safe(data["output"])
111
+
112
+ if "llm_parameters" in data:
113
+ data["llm_parameters"] = _make_json_safe(data["llm_parameters"])
114
+
115
+ return data
116
+
fluxloop/storage.py ADDED
@@ -0,0 +1,53 @@
1
+ """Offline storage helper for FluxLoop traces and observations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Iterable, Tuple
9
+ from uuid import UUID
10
+
11
+ from .config import get_config
12
+ from .models import ObservationData, TraceData
13
+ from .serialization import serialize_observation, serialize_trace
14
+
15
+
16
+ class OfflineStore:
17
+ """Persist traces and observations to local JSON artifacts."""
18
+
19
+ def __init__(self) -> None:
20
+ self.config = get_config()
21
+ self.base_dir = Path(self.config.offline_store_dir)
22
+ self.traces_file = self.base_dir / "traces.jsonl"
23
+ self.observations_file = self.base_dir / "observations.jsonl"
24
+
25
+ if self.config.offline_store_enabled:
26
+ self.base_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ def record_traces(self, traces: Iterable[TraceData]) -> None:
29
+ if not self.config.offline_store_enabled:
30
+ return
31
+
32
+ if not self.traces_file.exists():
33
+ self.traces_file.write_text("")
34
+
35
+ with self.traces_file.open("a") as fp:
36
+ for trace in traces:
37
+ fp.write(json.dumps(serialize_trace(trace)) + os.linesep)
38
+
39
+ def record_observations(
40
+ self, items: Iterable[Tuple[UUID, ObservationData]]
41
+ ) -> None:
42
+ if not self.config.offline_store_enabled:
43
+ return
44
+
45
+ if not self.observations_file.exists():
46
+ self.observations_file.write_text("")
47
+
48
+ with self.observations_file.open("a") as fp:
49
+ for trace_id, observation in items:
50
+ payload = serialize_observation(observation)
51
+ payload["trace_id"] = str(trace_id)
52
+ fp.write(json.dumps(payload) + os.linesep)
53
+
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: fluxloop
3
+ Version: 0.1.0
4
+ Summary: FluxLoop SDK for agent instrumentation and tracing
5
+ Author-email: FluxLoop Team <team@fluxloop.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/fluxloop/fluxloop
8
+ Project-URL: Documentation, https://docs.fluxloop.dev
9
+ Project-URL: Repository, https://github.com/fluxloop/fluxloop
10
+ Project-URL: Issues, https://github.com/fluxloop/fluxloop/issues
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: httpx>=0.24.0
24
+ Requires-Dist: python-dotenv>=1.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.0; extra == "dev"
31
+ Requires-Dist: black>=23.0; extra == "dev"
32
+ Provides-Extra: langchain
33
+ Requires-Dist: langchain>=0.1.0; extra == "langchain"
34
+ Provides-Extra: langgraph
35
+ Requires-Dist: langgraph>=0.0.20; extra == "langgraph"
36
+
37
+ # FluxLoop SDK
38
+
39
+ FluxLoop SDK for agent instrumentation and tracing.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install fluxloop
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ```python
50
+ from fluxloop import trace, FluxLoopClient
51
+
52
+ # Initialize the client
53
+ client = FluxLoopClient()
54
+
55
+ # Use the trace decorator
56
+ @trace()
57
+ def my_agent_function(prompt: str):
58
+ # Your agent logic here
59
+ return result
60
+ ```
61
+
62
+ ## Features
63
+
64
+ - 🔍 **Automatic Tracing**: Instrument your agent code with simple decorators
65
+ - 📊 **Rich Context**: Capture inputs, outputs, and metadata
66
+ - 🔄 **Async Support**: Works with both sync and async functions
67
+ - 🎯 **Framework Integration**: Built-in support for LangChain and LangGraph
68
+
69
+ ## Documentation
70
+
71
+ For detailed documentation, visit [https://docs.fluxloop.dev](https://docs.fluxloop.dev)
72
+
73
+ ## License
74
+
75
+ Apache License 2.0 - see LICENSE file for details
76
+
@@ -0,0 +1,17 @@
1
+ fluxloop/__init__.py,sha256=TomDcCLgeFB1LG3NpspyFVQ3bd5HNr4CPA2etl5XXKs,1248
2
+ fluxloop/buffer.py,sha256=pbviGFU0P_HlBYOlZccVuaj7OcQ5-M0L8d7r_vF1hQg,5940
3
+ fluxloop/client.py,sha256=4WCi1de8MJW-5La-ZalVYEaN7-gUFHlr6mByi1bBoc0,5487
4
+ fluxloop/config.py,sha256=A7hekvUFpQCeAcOEqj7d4fW8dvPihfaQ51ZbamiOyGw,5723
5
+ fluxloop/context.py,sha256=09VTTSnn72E0wRf55PX5j355ngKE2GXaqCYpgBVgxMM,6458
6
+ fluxloop/decorators.py,sha256=nfKIQpLwfMhr7A1TjtL-vppPWroPs98LsFB_1-ADfFM,15519
7
+ fluxloop/models.py,sha256=5AQ2jqArRGwQB91GOBVrBRb1h5gJBghl5G81EClOUp8,2399
8
+ fluxloop/recording.py,sha256=lnhJI0fWJfgMGMjw6TA8wU66PbelfYyD2UzMJ02W-uI,6609
9
+ fluxloop/serialization.py,sha256=SkY4bSUHxBrCkrPns-jWC1PkVSX8qh-oBprQTbmc5Z8,3239
10
+ fluxloop/storage.py,sha256=siwlbGdUpFAzdI6MMnODtkuxJyftQG9D4Phs2s5quMs,1732
11
+ fluxloop/schemas/__init__.py,sha256=przYFHTxNK7juJbqWZ2YVuCdBiMk3PB1H7T4xg3l62g,911
12
+ fluxloop/schemas/config.py,sha256=ib7GZVfWgUh9flwix9oKEOijyG0lWZjNBxC9lLbKZDw,11120
13
+ fluxloop/schemas/trace.py,sha256=tkeYQon9pCTZLp1Ad3e-KM86xLaofIqxjHJ7l-mX0Bk,6270
14
+ fluxloop-0.1.0.dist-info/METADATA,sha256=zeO_FZNKSlYYKAo-QDHZq2UJcbVqwWzGGfQ8ZSHWItM,2320
15
+ fluxloop-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ fluxloop-0.1.0.dist-info/top_level.txt,sha256=-whiUKvhn6Y7-TLfqrY7fZ1Cudjk8PnrKe7h7m8cuL4,9
17
+ fluxloop-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ fluxloop