prefactor-langchain 0.2.4__tar.gz → 0.2.7__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.
Files changed (18) hide show
  1. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/.gitignore +1 -1
  2. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/PKG-INFO +16 -9
  3. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/README.md +14 -7
  4. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/examples/README.md +1 -1
  5. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/examples/simple_agent.py +11 -5
  6. prefactor_langchain-0.2.7/examples/termination_demo.py +203 -0
  7. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/pyproject.toml +5 -2
  8. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/src/prefactor_langchain/__init__.py +2 -2
  9. prefactor_langchain-0.2.7/src/prefactor_langchain/_version.py +5 -0
  10. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/src/prefactor_langchain/middleware.py +170 -19
  11. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/tests/test_middleware.py +435 -8
  12. prefactor_langchain-0.2.7/tests/test_termination_demo.py +73 -0
  13. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/examples/.env.example +0 -0
  14. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/src/prefactor_langchain/metadata_extractor.py +0 -0
  15. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/src/prefactor_langchain/schemas.py +0 -0
  16. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/src/prefactor_langchain/spans.py +0 -0
  17. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/tests/test_metadata_extractor.py +0 -0
  18. {prefactor_langchain-0.2.4 → prefactor_langchain-0.2.7}/tests/test_schemas.py +0 -0
@@ -41,7 +41,6 @@ env/
41
41
 
42
42
  # uv
43
43
  .uv/
44
- uv.lock
45
44
 
46
45
  # Type checkers
47
46
  .mypy_cache/
@@ -70,3 +69,4 @@ htmlcov/
70
69
  .env
71
70
  mise.local.toml
72
71
  prefactor.json
72
+ .worktrees/
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefactor-langchain
3
- Version: 0.2.4
3
+ Version: 0.2.7
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.2.2
9
+ Requires-Dist: prefactor-core>=0.5.1
10
10
  Description-Content-Type: text/markdown
11
11
 
12
12
  # prefactor-langchain
@@ -27,9 +27,9 @@ pip install prefactor-langchain
27
27
  from prefactor_langchain import LangChainToolSchemaConfig, PrefactorMiddleware
28
28
 
29
29
  middleware = PrefactorMiddleware.from_config(
30
- api_url="https://api.prefactor.ai",
30
+ api_url="https://app.prefactorai.com",
31
31
  api_token="your-api-token",
32
- agent_id="my-agent",
32
+ agent_id="my-agent", # Optional for deployment-scoped tokens
33
33
  agent_name="My Agent", # optional
34
34
  tool_schemas={
35
35
  "send_email": LangChainToolSchemaConfig(
@@ -59,6 +59,9 @@ result = agent.invoke({"messages": [...]})
59
59
  await middleware.close()
60
60
  ```
61
61
 
62
+ With a deployment-scoped token you can omit `agent_id`; the backend derives the
63
+ agent and environment from the token during registration.
64
+
62
65
  ### Pre-configured client
63
66
 
64
67
  Pass a client you created yourself when you need full control over its
@@ -69,14 +72,16 @@ from prefactor_core import PrefactorCoreClient, PrefactorCoreConfig
69
72
  from prefactor_http.config import HttpClientConfig
70
73
  from prefactor_langchain import PrefactorMiddleware
71
74
 
72
- http_config = HttpClientConfig(api_url="https://api.prefactor.ai", api_token="your-api-token")
75
+ http_config = HttpClientConfig(
76
+ api_url="https://app.prefactorai.com", api_token="your-api-token"
77
+ )
73
78
  config = PrefactorCoreConfig(http_config=http_config)
74
79
  client = PrefactorCoreClient(config)
75
80
  await client.initialize()
76
81
 
77
82
  middleware = PrefactorMiddleware(
78
83
  client=client,
79
- agent_id="my-agent",
84
+ agent_id="my-agent", # Optional for deployment-scoped tokens
80
85
  agent_name="My Agent",
81
86
  )
82
87
 
@@ -121,9 +126,9 @@ register_langchain_schemas(
121
126
  )
122
127
 
123
128
  middleware = PrefactorMiddleware.from_config(
124
- api_url="https://api.prefactor.ai",
129
+ api_url="https://app.prefactorai.com",
125
130
  api_token="your-api-token",
126
- agent_id="my-agent",
131
+ agent_id="my-agent", # Optional for deployment-scoped tokens
127
132
  schema_registry=registry,
128
133
  )
129
134
  ```
@@ -140,7 +145,9 @@ from prefactor_core import PrefactorCoreClient, PrefactorCoreConfig
140
145
  from prefactor_http.config import HttpClientConfig
141
146
  from prefactor_langchain import PrefactorMiddleware
142
147
 
143
- http_config = HttpClientConfig(api_url="https://api.prefactor.ai", api_token="your-api-token")
148
+ http_config = HttpClientConfig(
149
+ api_url="https://app.prefactorai.com", api_token="your-api-token"
150
+ )
144
151
  config = PrefactorCoreConfig(http_config=http_config)
145
152
  client = PrefactorCoreClient(config)
146
153
  await client.initialize()
@@ -16,9 +16,9 @@ pip install prefactor-langchain
16
16
  from prefactor_langchain import LangChainToolSchemaConfig, PrefactorMiddleware
17
17
 
18
18
  middleware = PrefactorMiddleware.from_config(
19
- api_url="https://api.prefactor.ai",
19
+ api_url="https://app.prefactorai.com",
20
20
  api_token="your-api-token",
21
- agent_id="my-agent",
21
+ agent_id="my-agent", # Optional for deployment-scoped tokens
22
22
  agent_name="My Agent", # optional
23
23
  tool_schemas={
24
24
  "send_email": LangChainToolSchemaConfig(
@@ -48,6 +48,9 @@ result = agent.invoke({"messages": [...]})
48
48
  await middleware.close()
49
49
  ```
50
50
 
51
+ With a deployment-scoped token you can omit `agent_id`; the backend derives the
52
+ agent and environment from the token during registration.
53
+
51
54
  ### Pre-configured client
52
55
 
53
56
  Pass a client you created yourself when you need full control over its
@@ -58,14 +61,16 @@ from prefactor_core import PrefactorCoreClient, PrefactorCoreConfig
58
61
  from prefactor_http.config import HttpClientConfig
59
62
  from prefactor_langchain import PrefactorMiddleware
60
63
 
61
- http_config = HttpClientConfig(api_url="https://api.prefactor.ai", api_token="your-api-token")
64
+ http_config = HttpClientConfig(
65
+ api_url="https://app.prefactorai.com", api_token="your-api-token"
66
+ )
62
67
  config = PrefactorCoreConfig(http_config=http_config)
63
68
  client = PrefactorCoreClient(config)
64
69
  await client.initialize()
65
70
 
66
71
  middleware = PrefactorMiddleware(
67
72
  client=client,
68
- agent_id="my-agent",
73
+ agent_id="my-agent", # Optional for deployment-scoped tokens
69
74
  agent_name="My Agent",
70
75
  )
71
76
 
@@ -110,9 +115,9 @@ register_langchain_schemas(
110
115
  )
111
116
 
112
117
  middleware = PrefactorMiddleware.from_config(
113
- api_url="https://api.prefactor.ai",
118
+ api_url="https://app.prefactorai.com",
114
119
  api_token="your-api-token",
115
- agent_id="my-agent",
120
+ agent_id="my-agent", # Optional for deployment-scoped tokens
116
121
  schema_registry=registry,
117
122
  )
118
123
  ```
@@ -129,7 +134,9 @@ from prefactor_core import PrefactorCoreClient, PrefactorCoreConfig
129
134
  from prefactor_http.config import HttpClientConfig
130
135
  from prefactor_langchain import PrefactorMiddleware
131
136
 
132
- http_config = HttpClientConfig(api_url="https://api.prefactor.ai", api_token="your-api-token")
137
+ http_config = HttpClientConfig(
138
+ api_url="https://app.prefactorai.com", api_token="your-api-token"
139
+ )
133
140
  config = PrefactorCoreConfig(http_config=http_config)
134
141
  client = PrefactorCoreClient(config)
135
142
  await client.initialize()
@@ -36,7 +36,7 @@ pip install prefactor-langchain langchain
36
36
 
37
37
  ```bash
38
38
  export ANTHROPIC_API_KEY=your_api_key_here
39
- export PREFACTOR_API_URL=https://api.prefactor.ai
39
+ export PREFACTOR_API_URL=https://app.prefactorai.com
40
40
  export PREFACTOR_API_TOKEN=your_token_here
41
41
  ```
42
42
 
@@ -17,7 +17,7 @@ Prerequisites:
17
17
 
18
18
  Example:
19
19
  export ANTHROPIC_API_KEY=sk-ant-...
20
- export PREFACTOR_API_URL=https://api.prefactor.ai
20
+ export PREFACTOR_API_URL=https://app.prefactorai.com
21
21
  export PREFACTOR_API_TOKEN=your_token
22
22
  python simple_agent.py
23
23
  """
@@ -27,6 +27,7 @@ import asyncio
27
27
  import operator
28
28
  import os
29
29
  from datetime import datetime
30
+ from typing import Callable
30
31
 
31
32
  from langchain.agents import create_agent
32
33
  from langchain_core.tools import tool
@@ -38,7 +39,7 @@ from prefactor_langchain import PrefactorMiddleware
38
39
  # ---------------------------------------------------------------------------
39
40
 
40
41
 
41
- _OPS = {
42
+ _OPS: dict[type[ast.operator], Callable[[float, float], float]] = {
42
43
  ast.Add: operator.add,
43
44
  ast.Sub: operator.sub,
44
45
  ast.Mult: operator.mul,
@@ -48,9 +49,14 @@ _OPS = {
48
49
 
49
50
  def _safe_eval(node: ast.expr) -> float:
50
51
  if isinstance(node, ast.Constant):
51
- return node.n # type: ignore[return-value]
52
+ if isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
53
+ return float(node.value)
54
+ raise ValueError(f"Unsupported constant: {node.value!r}")
52
55
  if isinstance(node, ast.BinOp):
53
- return _OPS[type(node.op)](_safe_eval(node.left), _safe_eval(node.right)) # type: ignore[invalid-argument-type]
56
+ op_type = type(node.op)
57
+ if op_type in _OPS:
58
+ return _OPS[op_type](_safe_eval(node.left), _safe_eval(node.right))
59
+ raise ValueError(f"Unsupported operator: {ast.dump(node.op)}")
54
60
  if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
55
61
  return -_safe_eval(node.operand)
56
62
  raise ValueError(f"Unsupported expression: {ast.dump(node)}")
@@ -190,7 +196,7 @@ async def main_async():
190
196
  "Get your API key from: https://console.anthropic.com/"
191
197
  )
192
198
 
193
- api_url = os.getenv("PREFACTOR_API_URL", "https://api.prefactor.ai")
199
+ api_url = os.getenv("PREFACTOR_API_URL", "https://app.prefactorai.com")
194
200
  api_token = os.getenv("PREFACTOR_API_TOKEN")
195
201
  if not api_token:
196
202
  raise ValueError(
@@ -0,0 +1,203 @@
1
+ """Termination demo — runs a LangChain agent in a service loop and demonstrates
2
+ automatic detection of p2-initiated termination.
3
+
4
+ Required env vars:
5
+ PREFACTOR_API_URL e.g. http://localhost:4000
6
+ PREFACTOR_AGENT_ID agent ID on the target p2 instance
7
+ PREFACTOR_API_TOKEN deployment/BA token for SDK (span creation etc.)
8
+ PREFACTOR_BA_TOKEN BA token for terminate API (falls back to
9
+ PREFACTOR_API_TOKEN)
10
+
11
+ Optional env vars:
12
+ PREFACTOR_AUTO_TERMINATE_DELAY seconds before demo calls terminate API (default: 6)
13
+ PREFACTOR_RESTART_DELAY seconds to wait between runs (default: 75)
14
+
15
+ Usage:
16
+ PREFACTOR_API_URL=http://localhost:4000 \\
17
+ PREFACTOR_AGENT_ID=<agent-id> \\
18
+ PREFACTOR_API_TOKEN=<token> \\
19
+ PREFACTOR_AUTO_TERMINATE_DELAY=6 \\
20
+ PREFACTOR_RESTART_DELAY=75 \\
21
+ python packages/langchain/examples/termination_demo.py
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import logging
28
+ import os
29
+
30
+ import aiohttp
31
+ from langchain.agents import create_agent
32
+ from langchain_anthropic import ChatAnthropic
33
+ from langchain_core.tools import tool
34
+ from prefactor_core import PrefactorTerminatedError
35
+ from prefactor_langchain.middleware import PrefactorMiddleware
36
+
37
+ logging.basicConfig(
38
+ level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s"
39
+ )
40
+ logger = logging.getLogger("termination-demo")
41
+
42
+
43
+ @tool
44
+ def get_current_time() -> str:
45
+ """Return the current UTC time as a string."""
46
+ from datetime import datetime, timezone
47
+
48
+ return datetime.now(timezone.utc).isoformat()
49
+
50
+
51
+ async def terminate_after_delay(
52
+ api_url: str,
53
+ ba_token: str,
54
+ instance_id: str,
55
+ delay: float,
56
+ ) -> None:
57
+ """Call the terminate endpoint after a configured delay.
58
+
59
+ Args:
60
+ api_url: Base Prefactor API URL.
61
+ ba_token: Bearer token for terminate API authorization.
62
+ instance_id: Agent instance identifier to terminate.
63
+ delay: Seconds to wait before posting the terminate request.
64
+
65
+ Returns:
66
+ None.
67
+
68
+ Raises:
69
+ aiohttp.ClientResponseError: If the terminate API returns a non-2xx
70
+ response.
71
+ aiohttp.ClientError: If the terminate request fails before a response.
72
+ """
73
+ await asyncio.sleep(delay)
74
+ url = f"{api_url.rstrip('/')}/api/v1/agent_instance/{instance_id}/terminate"
75
+ logger.info("Calling terminate API: POST %s", url)
76
+ timeout = aiohttp.ClientTimeout(total=10)
77
+ async with aiohttp.ClientSession(timeout=timeout) as session:
78
+ async with session.post(
79
+ url,
80
+ headers={
81
+ "Authorization": f"Bearer {ba_token}",
82
+ "Content-Type": "application/json",
83
+ },
84
+ json={"reason": "demo termination"},
85
+ ) as resp:
86
+ body = await resp.text()
87
+ logger.info("Terminate API: status=%s body=%s", resp.status, body)
88
+ resp.raise_for_status()
89
+
90
+
91
+ async def _cancel_and_await_terminate_task(task: asyncio.Task[None]) -> None:
92
+ if not task.done():
93
+ task.cancel()
94
+ try:
95
+ await task
96
+ except asyncio.CancelledError:
97
+ pass
98
+ except Exception:
99
+ logger.exception("Auto-terminate task failed")
100
+ raise
101
+
102
+
103
+ async def run_once(
104
+ run_number: int,
105
+ api_url: str,
106
+ api_token: str,
107
+ agent_id: str,
108
+ auto_terminate_delay: float,
109
+ ) -> None:
110
+ """Run one termination-demo agent session.
111
+
112
+ Args:
113
+ run_number: Sequential run counter used in log messages.
114
+ api_url: Base Prefactor API URL.
115
+ api_token: SDK API token for Prefactor telemetry.
116
+ agent_id: Agent identifier used to initialize the middleware.
117
+ auto_terminate_delay: Seconds before the demo calls the terminate API.
118
+
119
+ Returns:
120
+ None.
121
+
122
+ Raises:
123
+ PrefactorTerminatedError: If Prefactor signals agent termination.
124
+ Exception: If agent invocation, termination cleanup, or middleware
125
+ cleanup fails.
126
+ """
127
+ environment_id = os.environ.get("PREFACTOR_ENVIRONMENT_ID")
128
+ middleware = PrefactorMiddleware.from_config(
129
+ api_url=api_url,
130
+ api_token=api_token,
131
+ agent_id=agent_id,
132
+ agent_name="termination-demo-agent",
133
+ environment_id=environment_id,
134
+ )
135
+
136
+ model = ChatAnthropic(model_name="claude-haiku-4-5-20251001")
137
+ agent = create_agent(
138
+ model, tools=[get_current_time], middleware=[middleware], checkpointer=None
139
+ )
140
+
141
+ instance = await middleware.ensure_initialized()
142
+ logger.info("Run #%d — Agent instance: %s", run_number, instance.id)
143
+ logger.info("Auto-terminate in %.0fs...", auto_terminate_delay)
144
+
145
+ ba_token = os.environ.get("PREFACTOR_BA_TOKEN", api_token)
146
+ terminate_task = asyncio.create_task(
147
+ terminate_after_delay(api_url, ba_token, instance.id, auto_terminate_delay)
148
+ )
149
+
150
+ try:
151
+ result = await agent.ainvoke(
152
+ {
153
+ "messages": [
154
+ {
155
+ "role": "user",
156
+ "content": "What time is it? Then keep asking every 2 seconds.",
157
+ }
158
+ ]
159
+ },
160
+ )
161
+ logger.info("Run #%d completed normally: %s", run_number, result)
162
+ except PrefactorTerminatedError as e:
163
+ logger.info("Run #%d terminated: %s", run_number, e)
164
+ raise
165
+ finally:
166
+ try:
167
+ await _cancel_and_await_terminate_task(terminate_task)
168
+ finally:
169
+ await middleware.close()
170
+
171
+
172
+ async def main() -> None:
173
+ """Run the termination demo restart loop.
174
+
175
+ Returns:
176
+ None.
177
+ """
178
+ api_url = os.environ["PREFACTOR_API_URL"]
179
+ api_token = os.environ["PREFACTOR_API_TOKEN"]
180
+ agent_id = os.environ["PREFACTOR_AGENT_ID"]
181
+ auto_terminate_delay = float(os.environ.get("PREFACTOR_AUTO_TERMINATE_DELAY", "6"))
182
+ restart_delay = float(os.environ.get("PREFACTOR_RESTART_DELAY", "75"))
183
+
184
+ run_number = 0
185
+ while True:
186
+ run_number += 1
187
+ try:
188
+ await run_once(
189
+ run_number, api_url, api_token, agent_id, auto_terminate_delay
190
+ )
191
+ except PrefactorTerminatedError:
192
+ logger.info("Service continues — next run in %.0fs.", restart_delay)
193
+ await asyncio.sleep(restart_delay)
194
+ except (KeyboardInterrupt, asyncio.CancelledError):
195
+ logger.info("Stopped by user.")
196
+ break
197
+ except Exception as e:
198
+ logger.exception("Unexpected error in run #%d: %s", run_number, e)
199
+ await asyncio.sleep(restart_delay)
200
+
201
+
202
+ if __name__ == "__main__":
203
+ asyncio.run(main())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prefactor-langchain"
3
- version = "0.2.4"
3
+ dynamic = ["version"]
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.2.2",
12
+ "prefactor-core>=0.5.1",
13
13
  "langchain-core>=1.0.0",
14
14
  ]
15
15
 
@@ -17,5 +17,8 @@ dependencies = [
17
17
  requires = ["hatchling"]
18
18
  build-backend = "hatchling.build"
19
19
 
20
+ [tool.hatch.version]
21
+ path = "src/prefactor_langchain/_version.py"
22
+
20
23
  [tool.hatch.build.targets.wheel]
21
24
  packages = ["src/prefactor_langchain"]
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from prefactor_langchain._version import __version__
5
6
  from prefactor_langchain.metadata_extractor import (
6
7
  extract_error_info,
7
8
  extract_token_usage,
@@ -25,8 +26,6 @@ from prefactor_langchain.spans import (
25
26
  ToolSpan,
26
27
  )
27
28
 
28
- __version__ = "0.2.4"
29
-
30
29
  __all__ = [
31
30
  # Middleware
32
31
  "PrefactorMiddleware",
@@ -48,4 +47,5 @@ __all__ = [
48
47
  "ToolSpan",
49
48
  "TokenUsage",
50
49
  "ErrorInfo",
50
+ "__version__",
51
51
  ]
@@ -0,0 +1,5 @@
1
+ """Package version for prefactor-langchain."""
2
+
3
+ PACKAGE_NAME = "prefactor-langchain"
4
+ __version__ = "0.2.7"
5
+ PACKAGE_VERSION = __version__