prefactor-langchain 0.2.0__tar.gz → 0.2.2__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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefactor-langchain
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: LangChain integration for Prefactor observability
5
5
  Author-email: Prefactor Pty Ltd <josh@prefactor.tech>
6
6
  License: MIT
7
7
  Requires-Python: <4.0.0,>=3.11.0
8
8
  Requires-Dist: langchain-core>=1.0.0
9
- Requires-Dist: prefactor-core>=0.1.0
9
+ Requires-Dist: prefactor-core>=0.2.1
10
10
  Description-Content-Type: text/markdown
11
11
 
12
12
  # prefactor-langchain
@@ -22,7 +22,9 @@ Example:
22
22
  python simple_agent.py
23
23
  """
24
24
 
25
+ import ast
25
26
  import asyncio
27
+ import operator
26
28
  import os
27
29
  from datetime import datetime
28
30
 
@@ -36,11 +38,29 @@ from prefactor_langchain import PrefactorMiddleware
36
38
  # ---------------------------------------------------------------------------
37
39
 
38
40
 
41
+ _OPS = {
42
+ ast.Add: operator.add,
43
+ ast.Sub: operator.sub,
44
+ ast.Mult: operator.mul,
45
+ ast.Div: operator.truediv,
46
+ }
47
+
48
+
49
+ def _safe_eval(node: ast.expr) -> float:
50
+ if isinstance(node, ast.Constant):
51
+ return node.n # type: ignore[return-value]
52
+ if isinstance(node, ast.BinOp):
53
+ return _OPS[type(node.op)](_safe_eval(node.left), _safe_eval(node.right))
54
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
55
+ return -_safe_eval(node.operand)
56
+ raise ValueError(f"Unsupported expression: {ast.dump(node)}")
57
+
58
+
39
59
  @tool
40
60
  def calculator(expression: str) -> str:
41
- """Evaluate a mathematical expression."""
61
+ """Evaluate a mathematical expression safely."""
42
62
  try:
43
- result = eval(expression) # noqa: S307
63
+ result = _safe_eval(ast.parse(expression, mode="eval").body)
44
64
  return f"Result: {result}"
45
65
  except Exception as e:
46
66
  return f"Error: {str(e)}"
@@ -100,7 +120,7 @@ async def run_agent_step(
100
120
  SpanContextStack propagates correctly so langchain:agent is automatically
101
121
  parented to workflow:agent_step.
102
122
  """
103
- instance = await middleware._ensure_initialized()
123
+ instance = await middleware.ensure_initialized()
104
124
 
105
125
  async with instance.span("workflow:agent_step") as ctx:
106
126
  await ctx.start({"message_count": len(messages)})
@@ -136,7 +156,7 @@ async def run_workflow(
136
156
  langchain:tool (if the agent calls a tool)
137
157
  workflow:post_process
138
158
  """
139
- instance = await middleware._ensure_initialized()
159
+ instance = await middleware.ensure_initialized()
140
160
 
141
161
  async with instance.span("workflow:run") as root:
142
162
  await root.start({"label": label, "query": query})
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prefactor-langchain"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "LangChain integration for Prefactor observability"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,7 +9,7 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.11.0, <4.0.0"
11
11
  dependencies = [
12
- "prefactor-core>=0.1.0",
12
+ "prefactor-core>=0.2.1",
13
13
  "langchain-core>=1.0.0",
14
14
  ]
15
15
 
@@ -20,7 +20,7 @@ from prefactor_langchain.spans import (
20
20
  ToolSpan,
21
21
  )
22
22
 
23
- __version__ = "0.2.0"
23
+ __version__ = "0.2.2"
24
24
 
25
25
  __all__ = [
26
26
  # Middleware
@@ -274,7 +274,7 @@ class PrefactorMiddleware(AgentMiddleware):
274
274
  # Derive a stable schema version ID from the registry + agent_name so
275
275
  # that the same configuration always maps to the same version
276
276
  # (idempotent across runs) and any change produces a new, distinct ID.
277
- agent_version_name = "langchain-agent"
277
+ agent_version_name = self._agent_name or "langchain-agent"
278
278
  schema_version_id = f"langchain-{self._agent_id}"
279
279
  if self._client._config.schema_registry is not None:
280
280
  raw = self._client._config.schema_registry.to_agent_schema_version(
@@ -303,6 +303,10 @@ class PrefactorMiddleware(AgentMiddleware):
303
303
  logger.debug("Initialized agent instance %s", self._instance.id)
304
304
  return self._instance
305
305
 
306
+ async def ensure_initialized(self) -> AgentInstanceHandle:
307
+ """Initialize the middleware and return the agent instance handle."""
308
+ return await self._ensure_initialized()
309
+
306
310
  async def close(self) -> None:
307
311
  """Close the middleware and cleanup resources.
308
312
 
@@ -580,6 +584,7 @@ class PrefactorMiddleware(AgentMiddleware):
580
584
  def _schedule() -> None:
581
585
  task = loop.create_task(_emit())
582
586
  pending.append(task)
587
+ task.add_done_callback(pending.remove)
583
588
 
584
589
  loop.call_soon_threadsafe(_schedule)
585
590
 
@@ -10,7 +10,7 @@ automatically, so they are not included in these span definitions.
10
10
 
11
11
  import traceback
12
12
  from dataclasses import dataclass, field
13
- from datetime import datetime
13
+ from datetime import datetime, timezone
14
14
  from typing import Any, Literal, Optional
15
15
 
16
16
 
@@ -63,7 +63,9 @@ class LangChainSpan:
63
63
  """
64
64
 
65
65
  name: str = "unnamed"
66
- start_time: float = field(default_factory=lambda: datetime.now().timestamp())
66
+ start_time: float = field(
67
+ default_factory=lambda: datetime.now(timezone.utc).timestamp()
68
+ )
67
69
  end_time: Optional[float] = None
68
70
  status: Literal["pending", "running", "completed", "failed"] = "pending"
69
71
  inputs: dict[str, Any] = field(default_factory=dict)
@@ -75,14 +77,14 @@ class LangChainSpan:
75
77
  def complete(self, outputs: Optional[dict[str, Any]] = None) -> None:
76
78
  """Mark the span as completed with outputs."""
77
79
  self.status = "completed"
78
- self.end_time = datetime.now().timestamp()
80
+ self.end_time = datetime.now(timezone.utc).timestamp()
79
81
  if outputs:
80
82
  self.outputs = outputs
81
83
 
82
84
  def fail(self, error: Exception) -> None:
83
85
  """Mark the span as failed with error information."""
84
86
  self.status = "failed"
85
- self.end_time = datetime.now().timestamp()
87
+ self.end_time = datetime.now(timezone.utc).timestamp()
86
88
  self.error = ErrorInfo(
87
89
  error_type=type(error).__name__,
88
90
  message=str(error),