prefactor-langchain 0.2.3__tar.gz → 0.2.4__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.
@@ -69,3 +69,4 @@ htmlcov/
69
69
  # Env
70
70
  .env
71
71
  mise.local.toml
72
+ prefactor.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefactor-langchain
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: LangChain integration for Prefactor observability
5
5
  Author-email: Prefactor Pty Ltd <josh@prefactor.tech>
6
6
  License: MIT
@@ -24,13 +24,26 @@ pip install prefactor-langchain
24
24
  ### Factory pattern (quickest setup)
25
25
 
26
26
  ```python
27
- from prefactor_langchain import PrefactorMiddleware
27
+ from prefactor_langchain import LangChainToolSchemaConfig, PrefactorMiddleware
28
28
 
29
29
  middleware = PrefactorMiddleware.from_config(
30
30
  api_url="https://api.prefactor.ai",
31
31
  api_token="your-api-token",
32
32
  agent_id="my-agent",
33
33
  agent_name="My Agent", # optional
34
+ tool_schemas={
35
+ "send_email": LangChainToolSchemaConfig(
36
+ span_type="send-email",
37
+ input_schema={
38
+ "type": "object",
39
+ "properties": {
40
+ "to": {"type": "string", "format": "email"},
41
+ "subject": {"type": "string"},
42
+ },
43
+ "required": ["to", "subject"],
44
+ },
45
+ )
46
+ },
34
47
  )
35
48
 
36
49
  # Use with LangChain's create_agent()
@@ -38,6 +51,7 @@ middleware = PrefactorMiddleware.from_config(
38
51
  # - Agent execution (langchain:agent)
39
52
  # - LLM calls (langchain:llm)
40
53
  # - Tool executions (langchain:tool)
54
+ # - Tool-specific executions (for example langchain:tool:send-email)
41
55
 
42
56
  result = agent.invoke({"messages": [...]})
43
57
 
@@ -73,6 +87,47 @@ await middleware.close() # closes the agent instance only
73
87
  await client.close()
74
88
  ```
75
89
 
90
+ ### SchemaRegistry composition
91
+
92
+ Use a shared ``SchemaRegistry`` when you want custom workflow span types and
93
+ LangChain tool schemas to be published together.
94
+
95
+ ```python
96
+ from prefactor_core import SchemaRegistry
97
+ from prefactor_langchain import (
98
+ LangChainToolSchemaConfig,
99
+ PrefactorMiddleware,
100
+ register_langchain_schemas,
101
+ )
102
+
103
+ registry = SchemaRegistry()
104
+ registry.register_type(
105
+ name="workflow:run",
106
+ params_schema={"type": "object"},
107
+ result_schema={"type": "object"},
108
+ )
109
+ register_langchain_schemas(
110
+ registry,
111
+ tool_schemas={
112
+ "send_email": LangChainToolSchemaConfig(
113
+ span_type="send-email",
114
+ input_schema={
115
+ "type": "object",
116
+ "properties": {"to": {"type": "string"}},
117
+ "required": ["to"],
118
+ },
119
+ )
120
+ },
121
+ )
122
+
123
+ middleware = PrefactorMiddleware.from_config(
124
+ api_url="https://api.prefactor.ai",
125
+ api_token="your-api-token",
126
+ agent_id="my-agent",
127
+ schema_registry=registry,
128
+ )
129
+ ```
130
+
76
131
  ### Pre-configured instance (spans outside the agent)
77
132
 
78
133
  Pass an `AgentInstanceHandle` you created yourself when you also need to
@@ -111,6 +166,12 @@ await instance.finish()
111
166
  await client.close()
112
167
  ```
113
168
 
169
+ If you pass ``tool_schemas=...`` with a pre-created instance, the middleware
170
+ uses those mappings to emit the right tool span types at runtime. The instance's
171
+ already-registered schema version is not mutated, so you must register matching
172
+ tool schemas before creating the instance if you want those per-tool span types
173
+ to appear in the backend schema version.
174
+
114
175
  ## Span Types
115
176
 
116
177
  This package creates LangChain-specific spans with the `langchain:*` namespace:
@@ -13,13 +13,26 @@ pip install prefactor-langchain
13
13
  ### Factory pattern (quickest setup)
14
14
 
15
15
  ```python
16
- from prefactor_langchain import PrefactorMiddleware
16
+ from prefactor_langchain import LangChainToolSchemaConfig, PrefactorMiddleware
17
17
 
18
18
  middleware = PrefactorMiddleware.from_config(
19
19
  api_url="https://api.prefactor.ai",
20
20
  api_token="your-api-token",
21
21
  agent_id="my-agent",
22
22
  agent_name="My Agent", # optional
23
+ tool_schemas={
24
+ "send_email": LangChainToolSchemaConfig(
25
+ span_type="send-email",
26
+ input_schema={
27
+ "type": "object",
28
+ "properties": {
29
+ "to": {"type": "string", "format": "email"},
30
+ "subject": {"type": "string"},
31
+ },
32
+ "required": ["to", "subject"],
33
+ },
34
+ )
35
+ },
23
36
  )
24
37
 
25
38
  # Use with LangChain's create_agent()
@@ -27,6 +40,7 @@ middleware = PrefactorMiddleware.from_config(
27
40
  # - Agent execution (langchain:agent)
28
41
  # - LLM calls (langchain:llm)
29
42
  # - Tool executions (langchain:tool)
43
+ # - Tool-specific executions (for example langchain:tool:send-email)
30
44
 
31
45
  result = agent.invoke({"messages": [...]})
32
46
 
@@ -62,6 +76,47 @@ await middleware.close() # closes the agent instance only
62
76
  await client.close()
63
77
  ```
64
78
 
79
+ ### SchemaRegistry composition
80
+
81
+ Use a shared ``SchemaRegistry`` when you want custom workflow span types and
82
+ LangChain tool schemas to be published together.
83
+
84
+ ```python
85
+ from prefactor_core import SchemaRegistry
86
+ from prefactor_langchain import (
87
+ LangChainToolSchemaConfig,
88
+ PrefactorMiddleware,
89
+ register_langchain_schemas,
90
+ )
91
+
92
+ registry = SchemaRegistry()
93
+ registry.register_type(
94
+ name="workflow:run",
95
+ params_schema={"type": "object"},
96
+ result_schema={"type": "object"},
97
+ )
98
+ register_langchain_schemas(
99
+ registry,
100
+ tool_schemas={
101
+ "send_email": LangChainToolSchemaConfig(
102
+ span_type="send-email",
103
+ input_schema={
104
+ "type": "object",
105
+ "properties": {"to": {"type": "string"}},
106
+ "required": ["to"],
107
+ },
108
+ )
109
+ },
110
+ )
111
+
112
+ middleware = PrefactorMiddleware.from_config(
113
+ api_url="https://api.prefactor.ai",
114
+ api_token="your-api-token",
115
+ agent_id="my-agent",
116
+ schema_registry=registry,
117
+ )
118
+ ```
119
+
65
120
  ### Pre-configured instance (spans outside the agent)
66
121
 
67
122
  Pass an `AgentInstanceHandle` you created yourself when you also need to
@@ -100,6 +155,12 @@ await instance.finish()
100
155
  await client.close()
101
156
  ```
102
157
 
158
+ If you pass ``tool_schemas=...`` with a pre-created instance, the middleware
159
+ uses those mappings to emit the right tool span types at runtime. The instance's
160
+ already-registered schema version is not mutated, so you must register matching
161
+ tool schemas before creating the instance if you want those per-tool span types
162
+ to appear in the backend schema version.
163
+
103
164
  ## Span Types
104
165
 
105
166
  This package creates LangChain-specific spans with the `langchain:*` namespace:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prefactor-langchain"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "LangChain integration for Prefactor observability"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -1,14 +1,19 @@
1
1
  """Prefactor LangChain - LangChain integration for Prefactor observability."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from prefactor_langchain.metadata_extractor import (
4
6
  extract_error_info,
5
7
  extract_token_usage,
6
8
  )
7
9
  from prefactor_langchain.middleware import PrefactorMiddleware
8
10
  from prefactor_langchain.schemas import (
11
+ DEFAULT_LANGCHAIN_AGENT_SCHEMA,
9
12
  LANGCHAIN_AGENT_SCHEMA,
10
13
  LANGCHAIN_LLM_SCHEMA,
11
14
  LANGCHAIN_TOOL_SCHEMA,
15
+ LangChainToolSchemaConfig,
16
+ compile_langchain_agent_schema,
12
17
  register_langchain_schemas,
13
18
  )
14
19
  from prefactor_langchain.spans import (
@@ -20,7 +25,7 @@ from prefactor_langchain.spans import (
20
25
  ToolSpan,
21
26
  )
22
27
 
23
- __version__ = "0.2.2"
28
+ __version__ = "0.2.4"
24
29
 
25
30
  __all__ = [
26
31
  # Middleware
@@ -30,6 +35,9 @@ __all__ = [
30
35
  "extract_error_info",
31
36
  # Schemas
32
37
  "register_langchain_schemas",
38
+ "compile_langchain_agent_schema",
39
+ "LangChainToolSchemaConfig",
40
+ "DEFAULT_LANGCHAIN_AGENT_SCHEMA",
33
41
  "LANGCHAIN_AGENT_SCHEMA",
34
42
  "LANGCHAIN_LLM_SCHEMA",
35
43
  "LANGCHAIN_TOOL_SCHEMA",
@@ -1,9 +1,12 @@
1
1
  """LangChain middleware for automatic tracing via prefactor-core."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import asyncio
4
6
  import hashlib
5
7
  import json
6
8
  import logging
9
+ from collections.abc import Mapping
7
10
  from typing import TYPE_CHECKING, Any, AsyncContextManager, Callable
8
11
 
9
12
  from langchain.agents.middleware import AgentMiddleware
@@ -20,7 +23,7 @@ if TYPE_CHECKING:
20
23
  pass
21
24
 
22
25
  from .metadata_extractor import extract_token_usage
23
- from .schemas import register_langchain_schemas
26
+ from .schemas import LangChainToolSchemaConfig, register_langchain_schemas
24
27
  from .spans import AgentSpan, LLMSpan, ToolSpan
25
28
 
26
29
  logger = logging.getLogger("prefactor_langchain.middleware")
@@ -111,6 +114,8 @@ class PrefactorMiddleware(AgentMiddleware):
111
114
  agent_id: str = "langchain-agent",
112
115
  agent_name: str | None = None,
113
116
  instance: AgentInstanceHandle | None = None,
117
+ tool_schemas: Mapping[str, LangChainToolSchemaConfig | Mapping[str, Any]]
118
+ | None = None,
114
119
  ):
115
120
  """Initialize the middleware with a pre-configured client or instance.
116
121
 
@@ -148,6 +153,9 @@ class PrefactorMiddleware(AgentMiddleware):
148
153
  self._current_parent_span_id: str | None = None
149
154
  self._loop: asyncio.AbstractEventLoop | None = None
150
155
  self._pending_emit_futures: list[asyncio.Task[None]] = []
156
+ self._tool_span_types = (
157
+ self._register_tool_schemas(None, tool_schemas) if tool_schemas else {}
158
+ )
151
159
  return
152
160
 
153
161
  if client is None:
@@ -179,6 +187,10 @@ class PrefactorMiddleware(AgentMiddleware):
179
187
  self._current_parent_span_id: str | None = None
180
188
  self._loop: asyncio.AbstractEventLoop | None = None
181
189
  self._pending_emit_futures: list[asyncio.Task[None]] = []
190
+ self._tool_span_types = {}
191
+
192
+ if tool_schemas:
193
+ self._tool_span_types = self._register_tool_schemas(client, tool_schemas)
182
194
 
183
195
  @classmethod
184
196
  def from_config(
@@ -189,6 +201,8 @@ class PrefactorMiddleware(AgentMiddleware):
189
201
  agent_name: str | None = None,
190
202
  schema_registry: SchemaRegistry | None = None,
191
203
  include_langchain_schemas: bool = True,
204
+ tool_schemas: Mapping[str, LangChainToolSchemaConfig | Mapping[str, Any]]
205
+ | None = None,
192
206
  ) -> "PrefactorMiddleware":
193
207
  """Factory method to create middleware from configuration.
194
208
 
@@ -203,6 +217,8 @@ class PrefactorMiddleware(AgentMiddleware):
203
217
  schema_registry: Optional SchemaRegistry for registering span schemas.
204
218
  include_langchain_schemas: If True and schema_registry is provided,
205
219
  automatically register LangChain-specific schemas.
220
+ tool_schemas: Optional per-tool schema configuration for tool-specific
221
+ span types.
206
222
 
207
223
  Returns:
208
224
  A configured PrefactorMiddleware instance with lazy initialization.
@@ -223,8 +239,12 @@ class PrefactorMiddleware(AgentMiddleware):
223
239
 
224
240
  # Create or augment schema registry
225
241
  registry = schema_registry or SchemaRegistry()
226
- if include_langchain_schemas and not registry.has_schema("langchain:llm"):
227
- register_langchain_schemas(registry)
242
+ tool_span_types: dict[str, str] = {}
243
+ if include_langchain_schemas or tool_schemas:
244
+ tool_span_types = register_langchain_schemas(
245
+ registry,
246
+ tool_schemas=tool_schemas,
247
+ )
228
248
 
229
249
  config = PrefactorCoreConfig(
230
250
  http_config=http_config,
@@ -245,9 +265,32 @@ class PrefactorMiddleware(AgentMiddleware):
245
265
  middleware._current_parent_span_id = None
246
266
  middleware._loop = None
247
267
  middleware._pending_emit_futures = []
268
+ middleware._tool_span_types = tool_span_types
248
269
  logger.debug("PrefactorMiddleware created via from_config()")
249
270
  return middleware
250
271
 
272
+ def _register_tool_schemas(
273
+ self,
274
+ client: PrefactorCoreClient | None,
275
+ tool_schemas: Mapping[str, LangChainToolSchemaConfig | Mapping[str, Any]],
276
+ ) -> dict[str, str]:
277
+ """Register tool schemas into a client registry when available."""
278
+ if client is None:
279
+ return register_langchain_schemas(
280
+ SchemaRegistry(),
281
+ tool_schemas=tool_schemas,
282
+ )
283
+
284
+ registry = client._config.schema_registry
285
+ if registry is None:
286
+ registry = SchemaRegistry()
287
+ client._config.schema_registry = registry
288
+
289
+ return register_langchain_schemas(
290
+ registry,
291
+ tool_schemas=tool_schemas,
292
+ )
293
+
251
294
  async def _ensure_initialized(self) -> AgentInstanceHandle:
252
295
  """Ensure the agent instance is initialized (lazily created if needed).
253
296
 
@@ -419,6 +462,18 @@ class PrefactorMiddleware(AgentMiddleware):
419
462
  "tool_name": tool_call.get("name", "unknown"),
420
463
  "arguments": tool_call.get("args", {}),
421
464
  }
465
+ if hasattr(request, "tool") and hasattr(request.tool, "name"):
466
+ return {
467
+ "tool_name": getattr(request.tool, "name", "unknown"),
468
+ "arguments": getattr(request, "args", {}),
469
+ }
470
+ if hasattr(request, "name"):
471
+ return {
472
+ "tool_name": request.name,
473
+ "arguments": getattr(
474
+ request, "input", getattr(request, "args", {})
475
+ ),
476
+ }
422
477
  return {}
423
478
  except Exception as e:
424
479
  logger.error("Error extracting tool inputs: %s", e, exc_info=True)
@@ -460,12 +515,14 @@ class PrefactorMiddleware(AgentMiddleware):
460
515
  result["error"] = span_data.error.to_dict()
461
516
  return result
462
517
 
463
- def _build_tool_params(self, span_data: Any) -> dict[str, Any]:
518
+ def _build_tool_params(self, span_data: Any, schema_name: str) -> dict[str, Any]:
464
519
  """Build the payload (params) dict for a tool span."""
465
520
  params: dict[str, Any] = {}
466
521
  if span_data.tool_name:
467
522
  params["tool_name"] = span_data.tool_name
468
- if span_data.inputs:
523
+ if schema_name != "langchain:tool":
524
+ params["inputs"] = span_data.arguments
525
+ elif span_data.inputs:
469
526
  params["inputs"] = span_data.inputs
470
527
  return params
471
528
 
@@ -554,7 +611,7 @@ class PrefactorMiddleware(AgentMiddleware):
554
611
  self._current_parent_span_id = span_id
555
612
  return span_id
556
613
 
557
- def _emit_child_span(self, span_data: Any) -> None:
614
+ def _emit_child_span(self, span_data: Any, schema_name: str | None = None) -> None:
558
615
  """Schedule a child span (LLM/tool) emission with parent tracking.
559
616
 
560
617
  Uses the current parent span ID from _current_parent_span_id.
@@ -566,15 +623,15 @@ class PrefactorMiddleware(AgentMiddleware):
566
623
  return
567
624
 
568
625
  instance = self._instance
569
- schema_name: str = span_data.type
626
+ schema_name = schema_name or span_data.type
570
627
  parent_span_id = self._current_parent_span_id
571
628
  pending = self._pending_emit_futures
572
629
 
573
630
  if schema_name == "langchain:llm":
574
631
  params = self._build_llm_params(span_data)
575
632
  result = self._build_llm_result(span_data)
576
- elif schema_name == "langchain:tool":
577
- params = self._build_tool_params(span_data)
633
+ elif schema_name.startswith("langchain:tool"):
634
+ params = self._build_tool_params(span_data, schema_name)
578
635
  result = self._build_tool_result(span_data)
579
636
  else:
580
637
  return
@@ -899,7 +956,9 @@ class PrefactorMiddleware(AgentMiddleware):
899
956
  type="langchain:tool",
900
957
  inputs=inputs,
901
958
  tool_name=tool_name,
959
+ arguments=inputs.get("arguments", {}),
902
960
  )
961
+ schema_name = self._resolve_tool_schema_name(tool_name)
903
962
 
904
963
  try:
905
964
  response = handler(request)
@@ -907,13 +966,13 @@ class PrefactorMiddleware(AgentMiddleware):
907
966
  outputs = self._extract_tool_output(response)
908
967
  span_data.complete(outputs=outputs)
909
968
 
910
- self._emit_child_span(span_data)
969
+ self._emit_child_span(span_data, schema_name=schema_name)
911
970
  logger.debug("Tool call completed (%s)", tool_name)
912
971
  return response
913
972
 
914
973
  except Exception as e:
915
974
  span_data.fail(e)
916
- self._emit_child_span(span_data)
975
+ self._emit_child_span(span_data, schema_name=schema_name)
917
976
  logger.error("Tool call failed: %s", e, exc_info=True)
918
977
  raise
919
978
 
@@ -941,10 +1000,12 @@ class PrefactorMiddleware(AgentMiddleware):
941
1000
  type="langchain:tool",
942
1001
  inputs=inputs,
943
1002
  tool_name=tool_name,
1003
+ arguments=inputs.get("arguments", {}),
944
1004
  )
1005
+ schema_name = self._resolve_tool_schema_name(tool_name)
945
1006
 
946
- async with instance.span("langchain:tool") as ctx:
947
- await ctx.start(self._build_tool_params(span_data))
1007
+ async with instance.span(schema_name) as ctx:
1008
+ await ctx.start(self._build_tool_params(span_data, schema_name))
948
1009
  try:
949
1010
  response = await handler(request)
950
1011
 
@@ -959,3 +1020,7 @@ class PrefactorMiddleware(AgentMiddleware):
959
1020
  await ctx.fail(self._build_tool_result(span_data))
960
1021
  logger.error("Tool call failed: %s", e, exc_info=True)
961
1022
  raise
1023
+
1024
+ def _resolve_tool_schema_name(self, tool_name: str) -> str:
1025
+ """Resolve a tool's configured span type, if one exists."""
1026
+ return self._tool_span_types.get(tool_name, "langchain:tool")