langgraph-api 0.4.11__tar.gz → 0.4.15__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.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/PKG-INFO +2 -2
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/ramp.js +18 -23
- langgraph_api-0.4.15/langgraph_api/__init__.py +1 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/a2a.py +1 -1
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/meta.py +35 -23
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/asyncio.py +4 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/langsmith/backend.py +1 -1
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/remote.py +1 -1
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/queue_entrypoint.py +13 -3
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/serde.py +0 -19
- langgraph_api-0.4.15/langgraph_api/utils/cache.py +95 -0
- langgraph_api-0.4.15/langgraph_api/utils/retriable_client.py +74 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/worker.py +1 -1
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/pyproject.toml +1 -1
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/uv.lock +38 -38
- langgraph_api-0.4.11/langgraph_api/__init__.py +0 -1
- langgraph_api-0.4.11/langgraph_api/utils/cache.py +0 -58
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/.gitignore +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/LICENSE +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/Makefile +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/README.md +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/.gitignore +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/Makefile +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/README.md +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/burst.js +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/clean.js +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/graphs.js +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/package.json +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/update-revision.js +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/benchmark/weather.js +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/constraints.txt +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/forbidden.txt +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/healthcheck.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/assistants.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/mcp.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/openapi.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/runs.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/store.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/threads.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/api/ui.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/asgi_transport.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/custom.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/langsmith/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/langsmith/client.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/middleware.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/noop.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/auth/studio_user.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/cli.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/command.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/config.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/cron_scheduler.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/errors.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/executor_entrypoint.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/feature_flags.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/graph.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/http.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/http_metrics.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/.gitignore +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/.prettierrc +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/base.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/build.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/client.http.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/client.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/errors.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/global.d.ts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/package.json +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/schema.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/graph.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/load.hooks.mjs +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/preload.mjs +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/files.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/importMap.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/src/utils/serde.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/sse.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/traceblock.mts +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/tsconfig.json +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/ui.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/js/yarn.lock +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/logging.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/metadata.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/http_logger.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/private_network.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/middleware/request_id.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/models/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/models/run.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/patch.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/route.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/schema.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/server.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/sse.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/state.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/store.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/stream.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/thread_ttl.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/traceblock.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/tunneling/cloudflare.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/config.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/future.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/headers.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/utils/uuids.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/validation.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_api/webhook.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_license/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_license/validation.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/__init__.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/checkpoint.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/database.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/lifespan.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/metrics.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/ops.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/queue.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/retry.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/langgraph_runtime/store.py +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/logging.json +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/openapi.json +0 -0
- {langgraph_api-0.4.11 → langgraph_api-0.4.15}/scripts/create_license.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.15
|
|
4
4
|
Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -11,7 +11,7 @@ Requires-Dist: httpx>=0.25.0
|
|
|
11
11
|
Requires-Dist: jsonschema-rs<0.30,>=0.20.0
|
|
12
12
|
Requires-Dist: langchain-core>=0.3.64
|
|
13
13
|
Requires-Dist: langgraph-checkpoint>=2.0.23
|
|
14
|
-
Requires-Dist: langgraph-runtime-inmem<0.
|
|
14
|
+
Requires-Dist: langgraph-runtime-inmem<0.13.0,>=0.12.0
|
|
15
15
|
Requires-Dist: langgraph-sdk>=0.2.0
|
|
16
16
|
Requires-Dist: langgraph>=0.4.0
|
|
17
17
|
Requires-Dist: langsmith>=0.3.45
|
|
@@ -56,7 +56,7 @@ export let options = {
|
|
|
56
56
|
},
|
|
57
57
|
thresholds: {
|
|
58
58
|
'run_duration': [`p(95)<${p95_run_duration[MODE]}`],
|
|
59
|
-
'successful_runs': [`count>${(PLATEAU_DURATION / (p95_run_duration[MODE] / 1000)) * LOAD_SIZE * LEVELS * 2}`], // Number of expected successful runs per user worst
|
|
59
|
+
'successful_runs': [`count>${(PLATEAU_DURATION / (p95_run_duration[MODE] / 1000)) * LOAD_SIZE * LEVELS * 2}`], // Number of expected successful runs per user worst case during plateau * max number of users * 2 cause that feels about right
|
|
60
60
|
'http_req_failed': ['rate<0.01'], // Error rate should be less than 1%
|
|
61
61
|
},
|
|
62
62
|
};
|
|
@@ -109,10 +109,16 @@ export default function() {
|
|
|
109
109
|
|
|
110
110
|
// Check the response
|
|
111
111
|
const expected_length = MODE === 'single' ? 1 : EXPAND + 1;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
let success = false;
|
|
113
|
+
try {
|
|
114
|
+
success = check(response, {
|
|
115
|
+
'Run completed successfully': (r) => r.status === 200,
|
|
116
|
+
'Response contains expected number of messages': (r) => JSON.parse(r.body)?.messages?.length === expected_length,
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.log(`Error checking response: ${error}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
|
|
117
123
|
if (success) {
|
|
118
124
|
// Record success metrics
|
|
@@ -126,34 +132,23 @@ export default function() {
|
|
|
126
132
|
if (response.status >= 500) {
|
|
127
133
|
serverErrors.add(1);
|
|
128
134
|
console.log(`Server error: ${response.status}`);
|
|
129
|
-
} else if (response.status === 408 || response.error
|
|
135
|
+
} else if (response.status === 408 || response.error?.includes('timeout')) {
|
|
130
136
|
timeoutErrors.add(1);
|
|
131
137
|
console.log(`Timeout error: ${response.error}`);
|
|
132
|
-
} else if (response.status === 200 && response
|
|
138
|
+
} else if (response.status === 200 && response.body?.messages?.length !== expected_length) {
|
|
133
139
|
missingMessageErrors.add(1);
|
|
134
|
-
console.log(response);
|
|
135
|
-
console.log(`Missing message error: Status ${response.status}, ${JSON.stringify(response.body)}`);
|
|
140
|
+
console.log(`Missing message error: Status ${response.status}, ${JSON.stringify(response.body)}, ${response.headers?.['Content-Location']}`);
|
|
136
141
|
} else {
|
|
137
142
|
otherErrors.add(1);
|
|
138
143
|
console.log(`Other error: Status ${response.status}, ${JSON.stringify(response.body)}`);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
} catch (error) {
|
|
142
|
-
// Handle
|
|
147
|
+
// Handle truly unexpected errors
|
|
143
148
|
failedRuns.add(1);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
console.log(`Timeout error: ${error.message}`);
|
|
148
|
-
} else if (error.message.includes('connection') || error.message.includes('network')) {
|
|
149
|
-
connectionErrors.add(1);
|
|
150
|
-
console.log(`Connection error: ${error.message}`);
|
|
151
|
-
} else {
|
|
152
|
-
otherErrors.add(1);
|
|
153
|
-
// Usually we end up with HTML error pages here
|
|
154
|
-
console.log(response);
|
|
155
|
-
console.log(`Unexpected error: ${error.message}, Response Body: ${response?.body}`);
|
|
156
|
-
}
|
|
149
|
+
otherErrors.add(1);
|
|
150
|
+
console.log(response);
|
|
151
|
+
console.log(`Unexpected error: ${error.message}, Response Body: ${response?.body}`);
|
|
157
152
|
}
|
|
158
153
|
|
|
159
154
|
// Add a small random sleep between iterations to prevent thundering herd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.15"
|
|
@@ -171,7 +171,7 @@ async def _validate_supports_messages(
|
|
|
171
171
|
"""
|
|
172
172
|
assistant_id = assistant["assistant_id"]
|
|
173
173
|
|
|
174
|
-
cached_schemas = _assistant_schemas_cache.get(assistant_id)
|
|
174
|
+
cached_schemas = await _assistant_schemas_cache.get(assistant_id)
|
|
175
175
|
if cached_schemas is not None:
|
|
176
176
|
schemas = cached_schemas
|
|
177
177
|
else:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import cast
|
|
2
2
|
|
|
3
3
|
import langgraph.version
|
|
4
|
+
import structlog
|
|
4
5
|
from starlette.responses import JSONResponse, PlainTextResponse
|
|
5
6
|
|
|
6
7
|
from langgraph_api import __version__, config, metadata
|
|
@@ -13,6 +14,8 @@ from langgraph_runtime.ops import Runs
|
|
|
13
14
|
|
|
14
15
|
METRICS_FORMATS = {"prometheus", "json"}
|
|
15
16
|
|
|
17
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
async def meta_info(request: ApiRequest):
|
|
18
21
|
plus = plus_features_enabled()
|
|
@@ -71,35 +74,44 @@ async def meta_metrics(request: ApiRequest):
|
|
|
71
74
|
resp["workers"] = worker_metrics
|
|
72
75
|
return JSONResponse(resp)
|
|
73
76
|
elif metrics_format == "prometheus":
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"# HELP lg_api_num_pending_runs The number of runs currently pending.",
|
|
79
|
-
"# TYPE lg_api_num_pending_runs gauge",
|
|
80
|
-
f'lg_api_num_pending_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_pending"]}',
|
|
81
|
-
"# HELP lg_api_num_running_runs The number of runs currently running.",
|
|
82
|
-
"# TYPE lg_api_num_running_runs gauge",
|
|
83
|
-
f'lg_api_num_running_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_running"]}',
|
|
84
|
-
]
|
|
77
|
+
metrics = []
|
|
78
|
+
try:
|
|
79
|
+
async with connect() as conn:
|
|
80
|
+
queue_stats = await Runs.stats(conn)
|
|
85
81
|
|
|
86
|
-
if config.N_JOBS_PER_WORKER > 0:
|
|
87
82
|
metrics.extend(
|
|
88
83
|
[
|
|
89
|
-
"# HELP
|
|
90
|
-
"# TYPE
|
|
91
|
-
f'
|
|
92
|
-
"# HELP
|
|
93
|
-
"# TYPE
|
|
94
|
-
f'
|
|
95
|
-
"# HELP lg_api_workers_available The number of available (idle) workers.",
|
|
96
|
-
"# TYPE lg_api_workers_available gauge",
|
|
97
|
-
f'lg_api_workers_available{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_available}',
|
|
84
|
+
"# HELP lg_api_num_pending_runs The number of runs currently pending.",
|
|
85
|
+
"# TYPE lg_api_num_pending_runs gauge",
|
|
86
|
+
f'lg_api_num_pending_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_pending"]}',
|
|
87
|
+
"# HELP lg_api_num_running_runs The number of runs currently running.",
|
|
88
|
+
"# TYPE lg_api_num_running_runs gauge",
|
|
89
|
+
f'lg_api_num_running_runs{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {queue_stats["n_running"]}',
|
|
98
90
|
]
|
|
99
91
|
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
# if we get a db connection error/timeout, just skip queue stats
|
|
94
|
+
await logger.awarning(
|
|
95
|
+
"Ignoring error while getting run stats for /metrics", exc_info=e
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if config.N_JOBS_PER_WORKER > 0:
|
|
99
|
+
metrics.extend(
|
|
100
|
+
[
|
|
101
|
+
"# HELP lg_api_workers_max The maximum number of workers available.",
|
|
102
|
+
"# TYPE lg_api_workers_max gauge",
|
|
103
|
+
f'lg_api_workers_max{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_max}',
|
|
104
|
+
"# HELP lg_api_workers_active The number of currently active workers.",
|
|
105
|
+
"# TYPE lg_api_workers_active gauge",
|
|
106
|
+
f'lg_api_workers_active{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_active}',
|
|
107
|
+
"# HELP lg_api_workers_available The number of available (idle) workers.",
|
|
108
|
+
"# TYPE lg_api_workers_available gauge",
|
|
109
|
+
f'lg_api_workers_available{{project_id="{metadata.PROJECT_ID}", revision_id="{metadata.HOST_REVISION_ID}"}} {workers_available}',
|
|
110
|
+
]
|
|
111
|
+
)
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
metrics.extend(http_metrics)
|
|
114
|
+
metrics.extend(pg_redis_stats)
|
|
103
115
|
|
|
104
116
|
metrics_response = "\n".join(metrics)
|
|
105
117
|
return PlainTextResponse(metrics_response)
|
|
@@ -158,6 +158,7 @@ class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
|
|
|
158
158
|
self,
|
|
159
159
|
*coros: Coroutine[Any, Any, T],
|
|
160
160
|
cancel: bool = False,
|
|
161
|
+
cancel_event: asyncio.Event | None = None,
|
|
161
162
|
wait: bool = True,
|
|
162
163
|
taskset: set[asyncio.Task] | None = None,
|
|
163
164
|
taskgroup_name: str | None = None,
|
|
@@ -165,6 +166,7 @@ class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
|
|
|
165
166
|
# Copy the taskset to avoid modifying the original set unintentionally (like in lifespan)
|
|
166
167
|
self.tasks = taskset.copy() if taskset is not None else set()
|
|
167
168
|
self.cancel = cancel
|
|
169
|
+
self.cancel_event = cancel_event
|
|
168
170
|
self.wait = wait
|
|
169
171
|
if taskset:
|
|
170
172
|
for task in tuple(taskset):
|
|
@@ -181,6 +183,8 @@ class SimpleTaskGroup(AbstractAsyncContextManager["SimpleTaskGroup"]):
|
|
|
181
183
|
try:
|
|
182
184
|
if (exc := task.exception()) and not isinstance(exc, ignore_exceptions):
|
|
183
185
|
logger.exception("asyncio.task failed in task group", exc_info=exc)
|
|
186
|
+
if self.cancel_event:
|
|
187
|
+
self.cancel_event.set()
|
|
184
188
|
except asyncio.CancelledError:
|
|
185
189
|
pass
|
|
186
190
|
|
|
@@ -58,7 +58,7 @@ class LangsmithAuthBackend(AuthenticationBackend):
|
|
|
58
58
|
|
|
59
59
|
# Check cache first
|
|
60
60
|
cache_key = self._get_cache_key(headers)
|
|
61
|
-
if cached_entry := self._cache.get(cache_key):
|
|
61
|
+
if cached_entry := await self._cache.get(cache_key):
|
|
62
62
|
return cached_entry["credentials"], cached_entry["user"]
|
|
63
63
|
|
|
64
64
|
async with auth_client() as auth:
|
|
@@ -895,7 +895,7 @@ class CustomJsAuthBackend(AuthenticationBackend):
|
|
|
895
895
|
if self.cache_keys:
|
|
896
896
|
cache_key = tuple((k, headers[k]) for k in self.cache_keys if k in headers)
|
|
897
897
|
if cache_key and self.ttl_cache is not None:
|
|
898
|
-
cached = self.ttl_cache.get(cache_key)
|
|
898
|
+
cached = await self.ttl_cache.get(cache_key)
|
|
899
899
|
if cached:
|
|
900
900
|
return cached
|
|
901
901
|
|
|
@@ -86,6 +86,7 @@ async def health_and_metrics_server():
|
|
|
86
86
|
log_level="error",
|
|
87
87
|
access_log=False,
|
|
88
88
|
)
|
|
89
|
+
# Server will run indefinitely until the process is terminated
|
|
89
90
|
server = uvicorn.Server(config)
|
|
90
91
|
|
|
91
92
|
logger.info(f"Health and metrics server started at http://0.0.0.0:{port}")
|
|
@@ -93,14 +94,15 @@ async def health_and_metrics_server():
|
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
async def entrypoint(
|
|
96
|
-
grpc_port: int | None = None,
|
|
97
|
+
grpc_port: int | None = None,
|
|
98
|
+
entrypoint_name: str = "python-queue",
|
|
99
|
+
cancel_event: asyncio.Event | None = None,
|
|
97
100
|
):
|
|
98
101
|
from langgraph_api import logging as lg_logging
|
|
99
102
|
from langgraph_api.api import user_router
|
|
100
103
|
|
|
101
104
|
lg_logging.set_logging_context({"entrypoint": entrypoint_name})
|
|
102
105
|
tasks: set[asyncio.Task] = set()
|
|
103
|
-
tasks.add(asyncio.create_task(health_and_metrics_server()))
|
|
104
106
|
|
|
105
107
|
original_lifespan = user_router.router.lifespan_context if user_router else None
|
|
106
108
|
|
|
@@ -113,6 +115,7 @@ async def entrypoint(
|
|
|
113
115
|
with_cron_scheduler=with_cron_scheduler,
|
|
114
116
|
grpc_port=grpc_port,
|
|
115
117
|
taskset=taskset,
|
|
118
|
+
cancel_event=cancel_event,
|
|
116
119
|
):
|
|
117
120
|
if original_lifespan:
|
|
118
121
|
async with original_lifespan(app):
|
|
@@ -123,6 +126,7 @@ async def entrypoint(
|
|
|
123
126
|
async with combined_lifespan(
|
|
124
127
|
None, with_cron_scheduler=False, grpc_port=grpc_port, taskset=tasks
|
|
125
128
|
):
|
|
129
|
+
tasks.add(asyncio.create_task(health_and_metrics_server()))
|
|
126
130
|
await asyncio.gather(*tasks)
|
|
127
131
|
|
|
128
132
|
|
|
@@ -141,8 +145,14 @@ async def main(grpc_port: int | None = None, entrypoint_name: str = "python-queu
|
|
|
141
145
|
signal.signal(signal.SIGTERM, lambda *_: _handle_signal())
|
|
142
146
|
|
|
143
147
|
entry_task = asyncio.create_task(
|
|
144
|
-
entrypoint(
|
|
148
|
+
entrypoint(
|
|
149
|
+
grpc_port=grpc_port,
|
|
150
|
+
entrypoint_name=entrypoint_name,
|
|
151
|
+
cancel_event=stop_event,
|
|
152
|
+
)
|
|
145
153
|
)
|
|
154
|
+
# Handle the case where the entrypoint errors out
|
|
155
|
+
entry_task.add_done_callback(lambda _: stop_event.set())
|
|
146
156
|
await stop_event.wait()
|
|
147
157
|
|
|
148
158
|
logger.warning("Cancelling queue entrypoint task")
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import base64
|
|
3
2
|
import re
|
|
4
3
|
import uuid
|
|
5
4
|
from base64 import b64encode
|
|
@@ -178,21 +177,3 @@ class Serializer(JsonPlusSerializer):
|
|
|
178
177
|
|
|
179
178
|
mpack_keys = {"method", "value"}
|
|
180
179
|
SERIALIZER = Serializer()
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# TODO: Make more performant (by removing)
|
|
184
|
-
async def reserialize_message(message: bytes) -> bytes:
|
|
185
|
-
# Stream messages from golang runtime are a byte dict of StreamChunks.
|
|
186
|
-
loaded = await ajson_loads(message)
|
|
187
|
-
converted = {}
|
|
188
|
-
for k, v in loaded.items():
|
|
189
|
-
if isinstance(v, dict) and v.keys() == mpack_keys:
|
|
190
|
-
if v["method"] == "missing":
|
|
191
|
-
converted[k] = v["value"] # oops
|
|
192
|
-
else:
|
|
193
|
-
converted[k] = SERIALIZER.loads_typed(
|
|
194
|
-
(v["method"], base64.b64decode(v["value"]))
|
|
195
|
-
)
|
|
196
|
-
else:
|
|
197
|
-
converted[k] = v
|
|
198
|
-
return json_dumpb(converted)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LRUCache(Generic[T]):
|
|
11
|
+
"""LRU cache with TTL and proactive refresh support."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
max_size: int = 1000,
|
|
16
|
+
ttl: float = 60,
|
|
17
|
+
refresh_window: float = 30,
|
|
18
|
+
refresh_callback: Callable[[str], Awaitable[T | None]] | None = None,
|
|
19
|
+
):
|
|
20
|
+
self._cache: OrderedDict[str, tuple[T, float, bool]] = OrderedDict()
|
|
21
|
+
self._max_size = max_size if max_size > 0 else 1000
|
|
22
|
+
self._ttl = ttl
|
|
23
|
+
self._refresh_window = refresh_window if refresh_window > 0 else 30
|
|
24
|
+
self._refresh_callback = refresh_callback
|
|
25
|
+
|
|
26
|
+
def _get_time(self) -> float:
|
|
27
|
+
"""Get current time, using loop.time() if available for better performance."""
|
|
28
|
+
try:
|
|
29
|
+
return asyncio.get_event_loop().time()
|
|
30
|
+
except RuntimeError:
|
|
31
|
+
return time.monotonic()
|
|
32
|
+
|
|
33
|
+
async def get(self, key: str) -> T | None:
|
|
34
|
+
"""Get item from cache, attempting refresh if within refresh window."""
|
|
35
|
+
if key not in self._cache:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
value, timestamp, is_refreshing = self._cache[key]
|
|
39
|
+
current_time = self._get_time()
|
|
40
|
+
time_until_expiry = self._ttl - (current_time - timestamp)
|
|
41
|
+
|
|
42
|
+
# Check if expired
|
|
43
|
+
if time_until_expiry <= 0:
|
|
44
|
+
del self._cache[key]
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Check if we should attempt refresh (within refresh window and not already refreshing)
|
|
48
|
+
if (
|
|
49
|
+
time_until_expiry <= self._refresh_window
|
|
50
|
+
and not is_refreshing
|
|
51
|
+
and self._refresh_callback
|
|
52
|
+
):
|
|
53
|
+
# Mark as refreshing to prevent multiple simultaneous refresh attempts
|
|
54
|
+
self._cache[key] = (value, timestamp, True)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Attempt refresh
|
|
58
|
+
refreshed_value = await self._refresh_callback(key)
|
|
59
|
+
if refreshed_value is not None:
|
|
60
|
+
# Refresh successful, update cache with new value
|
|
61
|
+
self._cache[key] = (refreshed_value, current_time, False)
|
|
62
|
+
# Move to end (most recently used)
|
|
63
|
+
self._cache.move_to_end(key)
|
|
64
|
+
return refreshed_value
|
|
65
|
+
else:
|
|
66
|
+
# Refresh failed, fallback to cached value
|
|
67
|
+
self._cache[key] = (value, timestamp, False)
|
|
68
|
+
except Exception:
|
|
69
|
+
# Refresh failed with exception, fallback to cached value
|
|
70
|
+
self._cache[key] = (value, timestamp, False)
|
|
71
|
+
|
|
72
|
+
# Move to end (most recently used)
|
|
73
|
+
self._cache.move_to_end(key)
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
def set(self, key: str, value: T) -> None:
|
|
77
|
+
"""Set item in cache, evicting old entries if needed."""
|
|
78
|
+
# Remove if already exists (to update timestamp)
|
|
79
|
+
if key in self._cache:
|
|
80
|
+
del self._cache[key]
|
|
81
|
+
|
|
82
|
+
# Evict oldest entries if needed
|
|
83
|
+
while len(self._cache) >= self._max_size:
|
|
84
|
+
self._cache.popitem(last=False) # Remove oldest (FIFO)
|
|
85
|
+
|
|
86
|
+
# Add new entry (not refreshing initially)
|
|
87
|
+
self._cache[key] = (value, self._get_time(), False)
|
|
88
|
+
|
|
89
|
+
def size(self) -> int:
|
|
90
|
+
"""Return current cache size."""
|
|
91
|
+
return len(self._cache)
|
|
92
|
+
|
|
93
|
+
def clear(self) -> None:
|
|
94
|
+
"""Clear all entries from cache."""
|
|
95
|
+
self._cache.clear()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import structlog
|
|
5
|
+
|
|
6
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def _make_http_request_with_retries(
|
|
10
|
+
url: str,
|
|
11
|
+
headers: dict,
|
|
12
|
+
method: str = "GET",
|
|
13
|
+
json_data: dict | None = None,
|
|
14
|
+
max_retries: int = 3,
|
|
15
|
+
base_delay: float = 1.0,
|
|
16
|
+
) -> httpx.Response | None:
|
|
17
|
+
"""
|
|
18
|
+
Make an HTTP request with exponential backoff retries.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
url: The URL to request
|
|
22
|
+
headers: Headers to include in the request
|
|
23
|
+
method: HTTP method ("GET" or "POST")
|
|
24
|
+
json_data: JSON data for POST requests
|
|
25
|
+
max_retries: Maximum number of retry attempts
|
|
26
|
+
base_delay: Base delay in seconds for exponential backoff
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
httpx.Response: The successful response
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
httpx.HTTPStatusError: If the request fails after all retries
|
|
33
|
+
httpx.RequestError: If the request fails after all retries
|
|
34
|
+
"""
|
|
35
|
+
for attempt in range(max_retries + 1):
|
|
36
|
+
try:
|
|
37
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
38
|
+
response = await client.request(
|
|
39
|
+
method, url, headers=headers, json=json_data
|
|
40
|
+
)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
except (
|
|
45
|
+
httpx.TimeoutException,
|
|
46
|
+
httpx.NetworkError,
|
|
47
|
+
httpx.RequestError,
|
|
48
|
+
httpx.HTTPStatusError,
|
|
49
|
+
) as e:
|
|
50
|
+
if isinstance(e, httpx.HTTPStatusError) and e.response.status_code < 500:
|
|
51
|
+
# Don't retry on 4xx errors, but do on 5xxs
|
|
52
|
+
raise e
|
|
53
|
+
|
|
54
|
+
# Back off and retry if we haven't reached the max retries
|
|
55
|
+
if attempt < max_retries:
|
|
56
|
+
delay = base_delay * (2**attempt) # Exponential backoff
|
|
57
|
+
logger.warning(
|
|
58
|
+
"HTTP %s request attempt %d to %s failed: %s. Retrying in %.1f seconds...",
|
|
59
|
+
method,
|
|
60
|
+
attempt + 1,
|
|
61
|
+
url,
|
|
62
|
+
e,
|
|
63
|
+
delay,
|
|
64
|
+
)
|
|
65
|
+
await asyncio.sleep(delay)
|
|
66
|
+
else:
|
|
67
|
+
logger.exception(
|
|
68
|
+
"HTTP %s request to %s failed after %d attempts. Last error: %s",
|
|
69
|
+
method,
|
|
70
|
+
url,
|
|
71
|
+
max_retries + 1,
|
|
72
|
+
e,
|
|
73
|
+
)
|
|
74
|
+
raise e
|
|
@@ -153,7 +153,7 @@ async def worker(
|
|
|
153
153
|
raise UserTimeout(e) from e
|
|
154
154
|
raise
|
|
155
155
|
|
|
156
|
-
async with Runs.enter(run_id, run["thread_id"], main_loop) as done:
|
|
156
|
+
async with Runs.enter(run_id, run["thread_id"], main_loop, resumable) as done:
|
|
157
157
|
# attempt the run
|
|
158
158
|
try:
|
|
159
159
|
if attempt > BG_JOB_MAX_RETRIES:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -381,7 +381,7 @@ wheels = [
|
|
|
381
381
|
|
|
382
382
|
[[package]]
|
|
383
383
|
name = "langgraph"
|
|
384
|
-
version = "0.6.
|
|
384
|
+
version = "0.6.7"
|
|
385
385
|
source = { registry = "https://pypi.org/simple" }
|
|
386
386
|
dependencies = [
|
|
387
387
|
{ name = "langchain-core" },
|
|
@@ -391,9 +391,9 @@ dependencies = [
|
|
|
391
391
|
{ name = "pydantic" },
|
|
392
392
|
{ name = "xxhash" },
|
|
393
393
|
]
|
|
394
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
394
|
+
sdist = { url = "https://files.pythonhosted.org/packages/56/85/36feb25062da40ca395f6c44d0232a672842e5421885101f6faf4670b670/langgraph-0.6.7.tar.gz", hash = "sha256:ba7fd17b8220142d6a4269b6038f2b3dcbcef42cd5ecf4a4c8d9b60b010830a6", size = 465534, upload-time = "2025-09-07T16:49:42.895Z" }
|
|
395
395
|
wheels = [
|
|
396
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
396
|
+
{ url = "https://files.pythonhosted.org/packages/67/06/f440922a58204dbfd10f7fdda0de0325529a159e9dc3d1038afe4b431a49/langgraph-0.6.7-py3-none-any.whl", hash = "sha256:c724dd8c24806b70faf4903e8e20c0234f8c0a356e0e96a88035cbecca9df2cf", size = 153329, upload-time = "2025-09-07T16:49:40.45Z" },
|
|
397
397
|
]
|
|
398
398
|
|
|
399
399
|
[[package]]
|
|
@@ -509,15 +509,15 @@ wheels = [
|
|
|
509
509
|
|
|
510
510
|
[[package]]
|
|
511
511
|
name = "langgraph-cli"
|
|
512
|
-
version = "0.4.
|
|
512
|
+
version = "0.4.2"
|
|
513
513
|
source = { registry = "https://pypi.org/simple" }
|
|
514
514
|
dependencies = [
|
|
515
515
|
{ name = "click" },
|
|
516
516
|
{ name = "langgraph-sdk" },
|
|
517
517
|
]
|
|
518
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
518
|
+
sdist = { url = "https://files.pythonhosted.org/packages/86/f1/598f9e1784432d790a937de4c466ba8bed3d18ef6f56fe7394af6bc1f175/langgraph_cli-0.4.2.tar.gz", hash = "sha256:074d93a2ebb9c60629a83bc4c149e837bd09e51222d48daacb498299d818ee9f", size = 778645, upload-time = "2025-09-05T22:55:03.37Z" }
|
|
519
519
|
wheels = [
|
|
520
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
520
|
+
{ url = "https://files.pythonhosted.org/packages/d3/35/92c5a0de3f08bbc245ba7c0b1d5f9a7edd025a1483bf4adde97864419825/langgraph_cli-0.4.2-py3-none-any.whl", hash = "sha256:d83b00f11f9840f153aeba5ad417b09cd7a5aa98ab4ad7f94e45fb089ed73785", size = 38045, upload-time = "2025-09-05T22:55:02.044Z" },
|
|
521
521
|
]
|
|
522
522
|
|
|
523
523
|
[[package]]
|
|
@@ -568,20 +568,20 @@ dev = [
|
|
|
568
568
|
|
|
569
569
|
[[package]]
|
|
570
570
|
name = "langgraph-sdk"
|
|
571
|
-
version = "0.2.
|
|
571
|
+
version = "0.2.6"
|
|
572
572
|
source = { registry = "https://pypi.org/simple" }
|
|
573
573
|
dependencies = [
|
|
574
574
|
{ name = "httpx" },
|
|
575
575
|
{ name = "orjson" },
|
|
576
576
|
]
|
|
577
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
577
|
+
sdist = { url = "https://files.pythonhosted.org/packages/55/35/a1caf4fdb725adec30f1e9562f218524a92d8b675deb97be653687f086ee/langgraph_sdk-0.2.6.tar.gz", hash = "sha256:7db27cd86d1231fa614823ff416fcd2541b5565ad78ae950f31ae96d7af7c519", size = 80346, upload-time = "2025-09-04T01:51:11.262Z" }
|
|
578
578
|
wheels = [
|
|
579
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
579
|
+
{ url = "https://files.pythonhosted.org/packages/c6/d2/c5fac919601b27a0af5df0bde46e7f1361d5e04505e404b75bed45d21fc8/langgraph_sdk-0.2.6-py3-none-any.whl", hash = "sha256:477216b573b8177bbd849f4c754782a81279fbbd88bfadfeda44422d14b18b08", size = 54565, upload-time = "2025-09-04T01:51:10.044Z" },
|
|
580
580
|
]
|
|
581
581
|
|
|
582
582
|
[[package]]
|
|
583
583
|
name = "langsmith"
|
|
584
|
-
version = "0.4.
|
|
584
|
+
version = "0.4.26"
|
|
585
585
|
source = { registry = "https://pypi.org/simple" }
|
|
586
586
|
dependencies = [
|
|
587
587
|
{ name = "httpx" },
|
|
@@ -592,9 +592,9 @@ dependencies = [
|
|
|
592
592
|
{ name = "requests-toolbelt" },
|
|
593
593
|
{ name = "zstandard" },
|
|
594
594
|
]
|
|
595
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
595
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f2/54/6e919bbad4761bc45805f623c1f75582388468e081367d4cc43a3fbd20f9/langsmith-0.4.26.tar.gz", hash = "sha256:1fab1f52c40a374ce7f391085bffb10511a9fd5035c1d6feebb222e1faea5845", size = 955076, upload-time = "2025-09-08T02:24:56.014Z" }
|
|
596
596
|
wheels = [
|
|
597
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
597
|
+
{ url = "https://files.pythonhosted.org/packages/b9/e8/5ec22fbe4323564c947eaebe4a3b010f1e0c80e8cbddb30fd5889007c4cb/langsmith-0.4.26-py3-none-any.whl", hash = "sha256:86eb83c21a1c3396eb2c7e02db4173fceda290ad8e1b7468ed0022c6aae2a220", size = 383852, upload-time = "2025-09-08T02:24:53.773Z" },
|
|
598
598
|
]
|
|
599
599
|
|
|
600
600
|
[[package]]
|
|
@@ -907,7 +907,7 @@ wheels = [
|
|
|
907
907
|
|
|
908
908
|
[[package]]
|
|
909
909
|
name = "pytest"
|
|
910
|
-
version = "8.4.
|
|
910
|
+
version = "8.4.2"
|
|
911
911
|
source = { registry = "https://pypi.org/simple" }
|
|
912
912
|
dependencies = [
|
|
913
913
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
@@ -916,9 +916,9 @@ dependencies = [
|
|
|
916
916
|
{ name = "pluggy" },
|
|
917
917
|
{ name = "pygments" },
|
|
918
918
|
]
|
|
919
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
919
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
|
920
920
|
wheels = [
|
|
921
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
921
|
+
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
|
922
922
|
]
|
|
923
923
|
|
|
924
924
|
[[package]]
|
|
@@ -1033,28 +1033,28 @@ wheels = [
|
|
|
1033
1033
|
|
|
1034
1034
|
[[package]]
|
|
1035
1035
|
name = "ruff"
|
|
1036
|
-
version = "0.12.
|
|
1037
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1038
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1039
|
-
wheels = [
|
|
1040
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1041
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1042
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1043
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1044
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1045
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1046
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1047
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1048
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1049
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1050
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1051
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1052
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1053
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1054
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1055
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1056
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1057
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1036
|
+
version = "0.12.12"
|
|
1037
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1038
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" }
|
|
1039
|
+
wheels = [
|
|
1040
|
+
{ url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" },
|
|
1041
|
+
{ url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" },
|
|
1042
|
+
{ url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" },
|
|
1043
|
+
{ url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" },
|
|
1044
|
+
{ url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" },
|
|
1045
|
+
{ url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" },
|
|
1046
|
+
{ url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" },
|
|
1047
|
+
{ url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" },
|
|
1048
|
+
{ url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" },
|
|
1049
|
+
{ url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" },
|
|
1050
|
+
{ url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" },
|
|
1051
|
+
{ url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" },
|
|
1052
|
+
{ url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" },
|
|
1053
|
+
{ url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" },
|
|
1054
|
+
{ url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" },
|
|
1055
|
+
{ url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" },
|
|
1056
|
+
{ url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" },
|
|
1057
|
+
{ url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" },
|
|
1058
1058
|
]
|
|
1059
1059
|
|
|
1060
1060
|
[[package]]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.11"
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import time
|
|
3
|
-
from collections import OrderedDict
|
|
4
|
-
from typing import Generic, TypeVar
|
|
5
|
-
|
|
6
|
-
T = TypeVar("T")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class LRUCache(Generic[T]):
|
|
10
|
-
"""LRU cache with TTL support."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, max_size: int = 1000, ttl: float = 60):
|
|
13
|
-
self._cache: OrderedDict[str, tuple[T, float]] = OrderedDict()
|
|
14
|
-
self._max_size = max_size if max_size > 0 else 1000
|
|
15
|
-
self._ttl = ttl
|
|
16
|
-
|
|
17
|
-
def _get_time(self) -> float:
|
|
18
|
-
"""Get current time, using loop.time() if available for better performance."""
|
|
19
|
-
try:
|
|
20
|
-
return asyncio.get_event_loop().time()
|
|
21
|
-
except RuntimeError:
|
|
22
|
-
return time.monotonic()
|
|
23
|
-
|
|
24
|
-
def get(self, key: str) -> T | None:
|
|
25
|
-
"""Get item from cache, returning None if expired or not found."""
|
|
26
|
-
if key not in self._cache:
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
value, timestamp = self._cache[key]
|
|
30
|
-
if self._get_time() - timestamp >= self._ttl:
|
|
31
|
-
# Expired, remove and return None
|
|
32
|
-
del self._cache[key]
|
|
33
|
-
return None
|
|
34
|
-
|
|
35
|
-
# Move to end (most recently used)
|
|
36
|
-
self._cache.move_to_end(key)
|
|
37
|
-
return value
|
|
38
|
-
|
|
39
|
-
def set(self, key: str, value: T) -> None:
|
|
40
|
-
"""Set item in cache, evicting old entries if needed."""
|
|
41
|
-
# Remove if already exists (to update timestamp)
|
|
42
|
-
if key in self._cache:
|
|
43
|
-
del self._cache[key]
|
|
44
|
-
|
|
45
|
-
# Evict oldest entries if needed
|
|
46
|
-
while len(self._cache) >= self._max_size:
|
|
47
|
-
self._cache.popitem(last=False) # Remove oldest (FIFO)
|
|
48
|
-
|
|
49
|
-
# Add new entry
|
|
50
|
-
self._cache[key] = (value, self._get_time())
|
|
51
|
-
|
|
52
|
-
def size(self) -> int:
|
|
53
|
-
"""Return current cache size."""
|
|
54
|
-
return len(self._cache)
|
|
55
|
-
|
|
56
|
-
def clear(self) -> None:
|
|
57
|
-
"""Clear all entries from cache."""
|
|
58
|
-
self._cache.clear()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|