langgraph-api 0.2.129__tar.gz → 0.2.132__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.

Files changed (121) hide show
  1. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/Makefile +2 -1
  2. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/PKG-INFO +2 -2
  3. langgraph_api-0.2.132/benchmark/.gitignore +10 -0
  4. langgraph_api-0.2.132/benchmark/Makefile +16 -0
  5. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/README.md +4 -2
  6. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/burst.js +2 -1
  7. langgraph_api-0.2.132/benchmark/graphs.js +236 -0
  8. langgraph_api-0.2.132/benchmark/package.json +17 -0
  9. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/ramp.js +28 -13
  10. langgraph_api-0.2.132/langgraph_api/__init__.py +1 -0
  11. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/assistants.py +6 -5
  12. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/meta.py +3 -1
  13. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/openapi.py +1 -1
  14. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/runs.py +13 -10
  15. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/ui.py +2 -0
  16. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/asgi_transport.py +2 -2
  17. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/asyncio.py +10 -8
  18. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/custom.py +9 -4
  19. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/langsmith/client.py +1 -1
  20. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/cli.py +5 -4
  21. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/config.py +2 -0
  22. langgraph_api-0.2.132/langgraph_api/executor_entrypoint.py +23 -0
  23. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/graph.py +25 -9
  24. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/http.py +10 -7
  25. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/http_metrics.py +4 -1
  26. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/base.py +0 -3
  27. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/build.mts +11 -2
  28. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/client.http.mts +2 -0
  29. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/client.mts +15 -11
  30. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/remote.py +22 -12
  31. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/preload.mjs +9 -1
  32. langgraph_api-0.2.132/langgraph_api/js/src/utils/files.mts +7 -0
  33. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/sse.py +1 -1
  34. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/logging.py +3 -3
  35. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/http_logger.py +4 -3
  36. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/models/run.py +20 -15
  37. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/patch.py +2 -2
  38. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/queue_entrypoint.py +33 -18
  39. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/route.py +7 -1
  40. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/schema.py +20 -1
  41. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/serde.py +32 -5
  42. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/server.py +5 -3
  43. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/state.py +8 -8
  44. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/store.py +1 -1
  45. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/stream.py +35 -20
  46. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/traceblock.py +1 -1
  47. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/__init__.py +21 -5
  48. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/config.py +13 -4
  49. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/future.py +1 -1
  50. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/headers.py +22 -5
  51. langgraph_api-0.2.132/langgraph_api/utils/uuids.py +87 -0
  52. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/webhook.py +20 -20
  53. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/worker.py +36 -9
  54. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/openapi.json +2 -2
  55. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/pyproject.toml +9 -11
  56. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/scripts/create_license.py +4 -3
  57. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/uv.lock +146 -140
  58. langgraph_api-0.2.129/benchmark/.gitignore +0 -3
  59. langgraph_api-0.2.129/benchmark/Makefile +0 -9
  60. langgraph_api-0.2.129/langgraph_api/__init__.py +0 -1
  61. langgraph_api-0.2.129/langgraph_api/js/src/utils/files.mts +0 -4
  62. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/.gitignore +0 -0
  63. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/LICENSE +0 -0
  64. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/README.md +0 -0
  65. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/benchmark/weather.js +0 -0
  66. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/constraints.txt +0 -0
  67. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/forbidden.txt +0 -0
  68. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/healthcheck.py +0 -0
  69. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/__init__.py +0 -0
  70. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/mcp.py +0 -0
  71. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/store.py +0 -0
  72. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/api/threads.py +0 -0
  73. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/__init__.py +0 -0
  74. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/langsmith/__init__.py +0 -0
  75. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/langsmith/backend.py +0 -0
  76. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/middleware.py +0 -0
  77. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/noop.py +0 -0
  78. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/auth/studio_user.py +0 -0
  79. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/command.py +0 -0
  80. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/cron_scheduler.py +0 -0
  81. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/errors.py +0 -0
  82. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/feature_flags.py +0 -0
  83. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/.gitignore +0 -0
  84. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/.prettierrc +0 -0
  85. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/__init__.py +0 -0
  86. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/errors.py +0 -0
  87. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/global.d.ts +0 -0
  88. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/package.json +0 -0
  89. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/schema.py +0 -0
  90. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/graph.mts +0 -0
  91. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/load.hooks.mjs +0 -0
  92. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/utils/importMap.mts +0 -0
  93. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  94. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/src/utils/serde.mts +0 -0
  95. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/traceblock.mts +0 -0
  96. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/tsconfig.json +0 -0
  97. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/ui.py +0 -0
  98. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/js/yarn.lock +0 -0
  99. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/metadata.py +0 -0
  100. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/__init__.py +0 -0
  101. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/private_network.py +0 -0
  102. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/middleware/request_id.py +0 -0
  103. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/models/__init__.py +0 -0
  104. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/sse.py +0 -0
  105. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/thread_ttl.py +0 -0
  106. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/tunneling/cloudflare.py +0 -0
  107. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils/cache.py +0 -0
  108. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/utils.py +0 -0
  109. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_api/validation.py +0 -0
  110. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_license/__init__.py +0 -0
  111. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_license/validation.py +0 -0
  112. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/__init__.py +0 -0
  113. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/checkpoint.py +0 -0
  114. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/database.py +0 -0
  115. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/lifespan.py +0 -0
  116. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/metrics.py +0 -0
  117. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/ops.py +0 -0
  118. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/queue.py +0 -0
  119. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/retry.py +0 -0
  120. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/langgraph_runtime/store.py +0 -0
  121. {langgraph_api-0.2.129 → langgraph_api-0.2.132}/logging.json +0 -0
@@ -5,6 +5,7 @@
5
5
  lint:
6
6
  uv run ruff check .
7
7
  uv run ruff format . --diff
8
+ uvx ty check --exclude "**/pb/**" --exclude "**/*_test.py" --exclude "**/test_*.py" --exclude "**/tests/**" --exclude "venv/**" --exclude ".venv/**" --exclude "build/**" --exclude "dist/**" .
8
9
 
9
10
  format:
10
11
  uv run ruff check --fix .
@@ -113,4 +114,4 @@ start-auth-fastapi-jwt:
113
114
  VERSION_KIND ?= patch
114
115
 
115
116
  bump-version:
116
- uv run --with hatch hatch version $(VERSION_KIND)
117
+ uv run --with hatch hatch version $(VERSION_KIND)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.2.129
3
+ Version: 0.2.132
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.7,>=0.6.9
14
+ Requires-Dist: langgraph-runtime-inmem<0.7,>=0.6.13
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
@@ -0,0 +1,10 @@
1
+ # K6 summary and results files
2
+ results_*.json
3
+ summary_*.json
4
+ raw_data_*.json
5
+
6
+ # Generated chart files
7
+ *_chart_*.png
8
+
9
+ # Node.js dependencies
10
+ node_modules/
@@ -0,0 +1,16 @@
1
+ # Benchmark commands
2
+ benchmark-burst:
3
+ k6 run burst.js
4
+
5
+ benchmark-ramp:
6
+ k6 run --out json=raw_data_$(shell date +%Y-%m-%dT%H-%M-%S).json ramp.js
7
+
8
+ benchmark-charts:
9
+ npm install
10
+ node graphs.js $(shell ls -t raw_data_*.json | head -1) true
11
+
12
+ benchmark-clean:
13
+ rm -f results_*.json summary_*.json raw_data_*.json *_chart_*.png
14
+
15
+ benchmark-clean-charts:
16
+ rm -f *_chart_*.png
@@ -12,8 +12,8 @@ There are two modes of testing available:
12
12
  BURST_SIZE - How many requests to run. Default: 100
13
13
  2. `Ramp` - Scale up the number of /run/wait requests and then plateau.
14
14
  Available Params:
15
- LOAD_SIZE - How much traffic to ramp up over a 60s period. Default: 100
16
- LEVELS - The number of times to ramp up. Default: 10
15
+ LOAD_SIZE - How much traffic to ramp up over a 60s period. Default: 500
16
+ LEVELS - The number of times to ramp up. Default: 2
17
17
  PLATEAU_DURATION - How long to sustain the max level of traffic in seconds. Default: 300
18
18
 
19
19
  ### Agent
@@ -65,6 +65,8 @@ make benchmark-clean
65
65
 
66
66
  Summary results are written to stdout and persisted in a summary_burst file. More detailed results for the same burst are persisted in a results_burst file.
67
67
 
68
+ Charts can be created from the run locally using the `make benchmark-charts` command.
69
+
68
70
  ## Resources
69
71
 
70
72
  - [K6 Documentation](https://k6.io/docs/)
@@ -139,7 +139,8 @@ export default function() {
139
139
  export function setup() {
140
140
  console.log(`Starting burst benchmark`);
141
141
  console.log(`Running on pod: ${__ENV.POD_NAME || 'local'}`);
142
- console.log(`Burst size: ${BURST_SIZE}`);
142
+ console.log(`Running with the following burst config: burst size ${BURST_SIZE}`);
143
+ console.log(`Running with the following agent config: data size ${DATA_SIZE}, delay ${DELAY}, expand ${EXPAND}, mode ${MODE}`);
143
144
 
144
145
  return { startTime: new Date().toISOString() };
145
146
  }
@@ -0,0 +1,236 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+ const QuickChart = require('quickchart-js');
5
+ const { plot } = require('nodeplotlib');
6
+
7
+ // Function to save chart using Quickchart
8
+ async function saveChartWithQuickchart(chartData, chartLayout, filename) {
9
+ const chart = new QuickChart();
10
+
11
+ chart.setWidth(800);
12
+ chart.setHeight(500);
13
+ chart.setFormat('png');
14
+
15
+ const config = {
16
+ type: 'line',
17
+ data: {
18
+ labels: chartData[0].x,
19
+ datasets: chartData.map(metric => {
20
+ return {
21
+ label: metric.name,
22
+ data: metric.y
23
+ }
24
+ })
25
+ }
26
+ };
27
+
28
+ chart.setConfig(config);
29
+
30
+ try {
31
+ const imageBuffer = await chart.toBinary();
32
+ fs.writeFileSync(filename, imageBuffer);
33
+ console.log(`Chart saved to ${filename}`);
34
+ } catch (error) {
35
+ console.error(`Error saving chart to ${filename}:`, error.message);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Function to generate charts from benchmark results
41
+ *
42
+ * Graphs that we generate:
43
+ * - VU scaling over time
44
+ * - Requests over time, broken into success and failure by reason
45
+ * - Average run duration over time
46
+ *
47
+ * Useful but currently in deployment dashboard:
48
+ * - Connection use over time (postgres, redis)
49
+ * - IOPS over time (postgres, redis)
50
+ * - Workers in use over time
51
+ *
52
+ * Dashboard: https://smith.langchain.com/o/ebbaf2eb-769b-4505-aca2-d11de10372a4/host/deployments/a23f03ff-6d4d-4efd-8149-bb5a7f3b95cf?tab=2&paginationModel=%7B%22pageIndex%22%3A0%2C%22pageSize%22%3A10%7D#
53
+ */
54
+ async function generateCharts(rawDataFile, displayInBrowser = false) {
55
+ console.log("Generating charts for", rawDataFile);
56
+ const aggregatedData = {};
57
+
58
+ // Read the results and summary files
59
+ const fileStream = fs.createReadStream(rawDataFile, 'utf8');
60
+ const rl = readline.createInterface({
61
+ input: fileStream,
62
+ crlfDelay: Infinity
63
+ });
64
+
65
+ for await (const line of rl) {
66
+ const entry = JSON.parse(line);
67
+
68
+ // Always written before the first point for that metric
69
+ if (entry.type === 'Metric') {
70
+ const metricName = entry.metric;
71
+ aggregatedData[metricName] = {};
72
+ } else if (entry.type === 'Point') {
73
+ const metricName = entry.metric;
74
+ const metricValue = entry.data.value;
75
+ const timestamp = Date.parse(entry.data.time);
76
+
77
+ // Round timestamp to 10-second intervals
78
+ const roundedTimestamp = Math.floor(timestamp / 10000) * 10000;
79
+
80
+ if (!aggregatedData[metricName][roundedTimestamp]) {
81
+ aggregatedData[metricName][roundedTimestamp] = [];
82
+ }
83
+ aggregatedData[metricName][roundedTimestamp].push(metricValue);
84
+ } else {
85
+ throw new Error(`Unexpected row type: ${entry.type}`);
86
+ }
87
+ }
88
+
89
+ // Convert aggregated data to final format with averages
90
+ const finalData = {};
91
+ for (const [metricName, timestampBuckets] of Object.entries(aggregatedData)) {
92
+ finalData[metricName] = [];
93
+ for (const [timestamp, values] of Object.entries(timestampBuckets)) {
94
+ const average = values.reduce((sum, val) => sum + val, 0) / values.length;
95
+ const max = Math.max(...values);
96
+ finalData[metricName].push({
97
+ timestamp: timestamp,
98
+ average: average,
99
+ max: max,
100
+ count: values.length
101
+ });
102
+ }
103
+ // Sort by timestamp
104
+ finalData[metricName].sort((a, b) => a.timestamp - b.timestamp);
105
+ }
106
+
107
+ // Create the charts
108
+ // We always get a http_reqs metric as one of the first
109
+ const firstTimestamp = finalData['http_reqs'][0].timestamp;
110
+
111
+ // Failed requests won't always be present
112
+ const data = [
113
+ {
114
+ x: finalData['http_reqs'].map(d => (d.timestamp - firstTimestamp) / 1000),
115
+ y: finalData['http_reqs'].map(d => d.count),
116
+ type: 'scatter',
117
+ mode: 'lines',
118
+ name: 'Total Requests',
119
+ },
120
+ {
121
+ x: finalData['successful_runs'].map(d => (d.timestamp - firstTimestamp) / 1000),
122
+ y: finalData['successful_runs'].map(d => d.count),
123
+ type: 'scatter',
124
+ mode: 'lines',
125
+ name: 'Success Rate',
126
+ }
127
+ ]
128
+ if (finalData['failed_runs']) {
129
+ data.push({
130
+ x: finalData['failed_runs'].map(d => (d.timestamp - firstTimestamp) / 1000),
131
+ y: finalData['failed_runs'].map(d => d.count),
132
+ type: 'scatter',
133
+ mode: 'lines',
134
+ name: 'Failed Requests',
135
+ })
136
+ }
137
+
138
+ const requestsChart = {
139
+ data,
140
+ layout: {
141
+ title: {
142
+ text: 'Success Rate Over Time',
143
+ },
144
+ xaxis: {
145
+ title: 'Time (10 second intervals)',
146
+ },
147
+ yaxis: {
148
+ title: 'Call Counts',
149
+ }
150
+ }
151
+ };
152
+
153
+ const runDurationChart = {
154
+ data: [
155
+ {
156
+ x: finalData['run_duration'].map(d => (d.timestamp - firstTimestamp) / 1000),
157
+ y: finalData['run_duration'].map(d => d.average),
158
+ type: 'scatter',
159
+ mode: 'lines',
160
+ name: 'Run Duration',
161
+ }
162
+ ],
163
+ layout: {
164
+ title: {
165
+ text: 'Run Duration Over Time',
166
+ },
167
+ xaxis: {
168
+ title: 'Time (10 second intervals)',
169
+ },
170
+ yaxis: {
171
+ title: 'Run Duration (ms)',
172
+ },
173
+ }
174
+ }
175
+
176
+ const vusChart = {
177
+ data: [
178
+ {
179
+ x: finalData['vus'].map(d => (d.timestamp - firstTimestamp) / 1000),
180
+ y: finalData['vus'].map(d => d.max),
181
+ type: 'scatter',
182
+ mode: 'lines',
183
+ name: 'Concurrent Virtual Users',
184
+ }
185
+ ],
186
+ layout: {
187
+ title: {
188
+ text: 'Concurrent Virtual Users over Time',
189
+ },
190
+ xaxis: {
191
+ title: 'Time (10 second intervals)',
192
+ },
193
+ yaxis: {
194
+ title: 'VUs (max)',
195
+ },
196
+ }
197
+ }
198
+
199
+ // Always save charts as images using Quickchart
200
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
201
+ const requestsImagePath = path.join(__dirname, `requests_chart_${timestamp}.png`);
202
+ const runDurationImagePath = path.join(__dirname, `run_duration_chart_${timestamp}.png`);
203
+ const vusImagePath = path.join(__dirname, `vus_chart_${timestamp}.png`);
204
+
205
+ // Save charts using Quickchart
206
+ await saveChartWithQuickchart(requestsChart.data, requestsChart.layout, requestsImagePath);
207
+ await saveChartWithQuickchart(runDurationChart.data, runDurationChart.layout, runDurationImagePath);
208
+ await saveChartWithQuickchart(vusChart.data, vusChart.layout, vusImagePath);
209
+
210
+ // Display in CLI if requested
211
+ if (displayInBrowser) {
212
+ plot(requestsChart.data, requestsChart.layout);
213
+ plot(runDurationChart.data, runDurationChart.layout);
214
+ plot(vusChart.data, vusChart.layout);
215
+ }
216
+
217
+ console.log(`Charts generated: ${vusImagePath}, ${requestsImagePath}, ${runDurationImagePath}`);
218
+ }
219
+
220
+ // CLI usage
221
+ if (require.main === module) {
222
+ const [,, rawDataFile, displayInBrowser] = process.argv;
223
+
224
+ if (!rawDataFile) {
225
+ console.error('Usage: node graphs.js <raw-data-file>');
226
+ process.exit(1);
227
+ }
228
+
229
+ generateCharts(rawDataFile, displayInBrowser)
230
+ .catch(error => {
231
+ console.error('Failed to generate charts:', error);
232
+ process.exit(1);
233
+ });
234
+ }
235
+
236
+ module.exports = { generateCharts };
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "benchmark-charts",
3
+ "version": "1.0.0",
4
+ "description": "Chart generation for K6 benchmark results",
5
+ "main": "graphs.js",
6
+ "scripts": {
7
+ "generate-charts": "node graphs.js"
8
+ },
9
+ "dependencies": {
10
+ "nodeplotlib": "^1.0.4",
11
+ "quickchart-js": "^3.1.3"
12
+ },
13
+ "devDependencies": {},
14
+ "engines": {
15
+ "node": ">=14.0.0"
16
+ }
17
+ }
@@ -18,9 +18,10 @@ const BASE_URL = __ENV.BASE_URL || 'http://localhost:9123';
18
18
  const LANGSMITH_API_KEY = __ENV.LANGSMITH_API_KEY;
19
19
 
20
20
  // Params for the runner
21
- const LOAD_SIZE = parseInt(__ENV.LOAD_SIZE || '100');
22
- const LEVELS = parseInt(__ENV.LEVELS || '10');
21
+ const LOAD_SIZE = parseInt(__ENV.LOAD_SIZE || '500');
22
+ const LEVELS = parseInt(__ENV.LEVELS || '2');
23
23
  const PLATEAU_DURATION = parseInt(__ENV.PLATEAU_DURATION || '300');
24
+ const STATEFUL = __ENV.STATEFUL === 'true';
24
25
 
25
26
  // Params for the agent
26
27
  const DATA_SIZE = parseInt(__ENV.DATA_SIZE || '1000');
@@ -33,7 +34,7 @@ for (let i = 1; i <= LEVELS; i++) {
33
34
  stages.push({ duration: '60s', target: LOAD_SIZE * i });
34
35
  }
35
36
  stages.push({ duration: `${PLATEAU_DURATION}s`, target: LOAD_SIZE * LEVELS});
36
- stages.push({ duration: '30s', target: 0 }); // Ramp down
37
+ stages.push({ duration: '60s', target: 0 }); // Ramp down
37
38
 
38
39
  // Test configuration
39
40
  export let options = {
@@ -46,9 +47,10 @@ export let options = {
46
47
  },
47
48
  },
48
49
  thresholds: {
49
- 'run_duration': ['p(95)<30000'], // 95% of runs should complete within 30s
50
- 'successful_runs': ['count>100'], // At least 100 successful runs
51
- 'http_req_failed': ['rate<0.2'], // Error rate should be less than 20%
50
+ // These are the first set of goals I'd like to hit. To get the job just working and ramp on main, leaving them off for now.
51
+ // 'run_duration': ['p(95)<10000'], // 95% of runs should complete within 10s
52
+ // 'successful_runs': ['count>100000'], // At least 100,000 successful runs
53
+ // 'http_req_failed': ['rate<0.05'], // Error rate should be less than 5%
52
54
  },
53
55
  };
54
56
 
@@ -75,10 +77,21 @@ export default function() {
75
77
  config: {
76
78
  recursion_limit: EXPAND + 2,
77
79
  },
80
+ });
81
+
82
+ // If the request is stateful, create a thread first and use it in the url
83
+ let url = `${BASE_URL}/runs/wait`;
84
+ if (STATEFUL) {
85
+ const thread = http.post(`${BASE_URL}/threads`, payload, {
86
+ headers,
87
+ timeout: '120s' // k6 request timeout slightly longer than the server timeout
78
88
  });
89
+ const threadId = thread.json().thread_id;
90
+ url = `${BASE_URL}/threads/${threadId}/runs/wait`;
91
+ }
79
92
 
80
93
  // Make a single request to the wait endpoint
81
- const response = http.post(`${BASE_URL}/runs/wait`, payload, {
94
+ const response = http.post(url, payload, {
82
95
  headers,
83
96
  timeout: '120s' // k6 request timeout slightly longer than the server timeout
84
97
  });
@@ -141,9 +154,10 @@ export default function() {
141
154
  export function setup() {
142
155
  console.log(`Starting ramp benchmark`);
143
156
  console.log(`Running on pod: ${__ENV.POD_NAME || 'local'}`);
144
- console.log(`Running ${LEVELS} levels with base size ${LOAD_SIZE}`);
157
+ console.log(`Running with the following ramp config: load size ${LOAD_SIZE}, levels ${LEVELS}, plateau duration ${PLATEAU_DURATION}, stateful ${STATEFUL}`);
158
+ console.log(`Running with the following agent config: data size ${DATA_SIZE}, delay ${DELAY}, expand ${EXPAND}, mode ${MODE}`);
145
159
 
146
- return { startTime: new Date().toISOString() };
160
+ return { startTime: new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '') };
147
161
  }
148
162
 
149
163
  // Handle summary
@@ -152,13 +166,14 @@ export function handleSummary(data) {
152
166
 
153
167
  // Create summary information with aggregated metrics
154
168
  const summary = {
155
- timestamp: timestamp,
169
+ startTimestamp: data.setup_data.startTime,
170
+ endTimestamp: timestamp,
156
171
  metrics: {
157
- totalRuns: data.metrics.successful_runs.values.count + data.metrics.failed_runs.values.count,
172
+ totalRuns: data.metrics.successful_runs.values.count + (data.metrics.failed_runs?.values?.count || 0),
158
173
  successfulRuns: data.metrics.successful_runs.values.count,
159
- failedRuns: data.metrics.failed_runs.values.count,
174
+ failedRuns: data.metrics.failed_runs?.values?.count || 0,
160
175
  successRate: data.metrics.successful_runs.values.count /
161
- (data.metrics.successful_runs.values.count + data.metrics.failed_runs.values.count) * 100,
176
+ (data.metrics.successful_runs.values.count + (data.metrics.failed_runs?.values?.count || 0)) * 100,
162
177
  averageDuration: data.metrics.run_duration.values.avg / 1000, // in seconds
163
178
  p95Duration: data.metrics.run_duration.values["p(95)"] / 1000, // in seconds
164
179
  errors: {
@@ -0,0 +1 @@
1
+ __version__ = "0.2.132"
@@ -61,7 +61,8 @@ def _get_configurable_jsonschema(graph: Pregel) -> dict:
61
61
  in favor of graph.get_context_jsonschema().
62
62
  """
63
63
  # Otherwise, use the config_schema method.
64
- config_schema = graph.config_schema()
64
+ # TODO: Remove this when we no longer support langgraph < 0.6
65
+ config_schema = graph.config_schema() # type: ignore[deprecated]
65
66
  model_fields = getattr(config_schema, "model_fields", None) or getattr(
66
67
  config_schema, "__fields__", None
67
68
  )
@@ -87,11 +88,11 @@ def _state_jsonschema(graph: Pregel) -> dict | None:
87
88
  for k in graph.stream_channels_list:
88
89
  v = graph.channels[k]
89
90
  try:
90
- create_model(k, __root__=(v.UpdateType, None)).schema()
91
+ create_model(k, __root__=(v.UpdateType, None)).model_json_schema()
91
92
  fields[k] = (v.UpdateType, None)
92
93
  except Exception:
93
94
  fields[k] = (Any, None)
94
- return create_model(graph.get_name("State"), **fields).schema()
95
+ return create_model(graph.get_name("State"), **fields).model_json_schema()
95
96
 
96
97
 
97
98
  def _graph_schemas(graph: Pregel) -> dict:
@@ -132,7 +133,7 @@ def _graph_schemas(graph: Pregel) -> dict:
132
133
  logger.warning(
133
134
  f"Failed to get context schema for graph {graph.name} with error: `{str(e)}`"
134
135
  )
135
- context_schema = graph.config_schema()
136
+ context_schema = graph.config_schema() # type: ignore[deprecated]
136
137
  else:
137
138
  context_schema = None
138
139
 
@@ -366,7 +367,7 @@ async def patch_assistant(
366
367
 
367
368
 
368
369
  @retry_db
369
- async def delete_assistant(request: ApiRequest) -> ApiResponse:
370
+ async def delete_assistant(request: ApiRequest) -> Response:
370
371
  """Delete an assistant by ID."""
371
372
  assistant_id = request.path_params["assistant_id"]
372
373
  validate_uuid(assistant_id, "Invalid assistant ID: must be a UUID")
@@ -1,3 +1,5 @@
1
+ from typing import cast
2
+
1
3
  import langgraph.version
2
4
  from starlette.responses import JSONResponse, PlainTextResponse
3
5
 
@@ -43,7 +45,7 @@ async def meta_metrics(request: ApiRequest):
43
45
 
44
46
  # collect stats
45
47
  metrics = get_metrics()
46
- worker_metrics = metrics["workers"]
48
+ worker_metrics = cast(dict[str, int], metrics["workers"])
47
49
  workers_max = worker_metrics["max"]
48
50
  workers_active = worker_metrics["active"]
49
51
  workers_available = worker_metrics["available"]
@@ -25,7 +25,7 @@ def set_custom_spec(spec: dict):
25
25
 
26
26
 
27
27
  @lru_cache(maxsize=1)
28
- def get_openapi_spec() -> str:
28
+ def get_openapi_spec() -> bytes:
29
29
  # patch the graph_id enums
30
30
  graph_ids = list(GRAPHS.keys())
31
31
  for schema in (
@@ -1,9 +1,8 @@
1
1
  import asyncio
2
2
  from collections.abc import AsyncIterator
3
- from typing import Literal
3
+ from typing import Literal, cast
4
4
 
5
5
  import orjson
6
- from langgraph.checkpoint.base.id import uuid6
7
6
  from starlette.exceptions import HTTPException
8
7
  from starlette.responses import Response, StreamingResponse
9
8
 
@@ -12,7 +11,7 @@ from langgraph_api.asyncio import ValueEvent, aclosing
12
11
  from langgraph_api.models.run import create_valid_run
13
12
  from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
14
13
  from langgraph_api.sse import EventSourceResponse
15
- from langgraph_api.utils import fetchone, get_pagination_headers, validate_uuid
14
+ from langgraph_api.utils import fetchone, get_pagination_headers, uuid7, validate_uuid
16
15
  from langgraph_api.validation import (
17
16
  CronCreate,
18
17
  CronSearch,
@@ -92,7 +91,7 @@ async def stream_run(
92
91
  thread_id = request.path_params["thread_id"]
93
92
  payload = await request.json(RunCreateStateful)
94
93
  on_disconnect = payload.get("on_disconnect", "continue")
95
- run_id = uuid6()
94
+ run_id = uuid7()
96
95
  sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
97
96
 
98
97
  try:
@@ -132,7 +131,7 @@ async def stream_run_stateless(
132
131
  """Create a stateless run."""
133
132
  payload = await request.json(RunCreateStateless)
134
133
  on_disconnect = payload.get("on_disconnect", "continue")
135
- run_id = uuid6()
134
+ run_id = uuid7()
136
135
  sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
137
136
 
138
137
  try:
@@ -173,7 +172,7 @@ async def wait_run(request: ApiRequest):
173
172
  thread_id = request.path_params["thread_id"]
174
173
  payload = await request.json(RunCreateStateful)
175
174
  on_disconnect = payload.get("on_disconnect", "continue")
176
- run_id = uuid6()
175
+ run_id = uuid7()
177
176
  sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
178
177
 
179
178
  try:
@@ -255,7 +254,7 @@ async def wait_run_stateless(request: ApiRequest):
255
254
  """Create a stateless run, wait for the output."""
256
255
  payload = await request.json(RunCreateStateless)
257
256
  on_disconnect = payload.get("on_disconnect", "continue")
258
- run_id = uuid6()
257
+ run_id = uuid7()
259
258
  sub = asyncio.create_task(Runs.Stream.subscribe(run_id))
260
259
 
261
260
  try:
@@ -425,7 +424,10 @@ async def cancel_run(
425
424
  wait_str = request.query_params.get("wait", "false")
426
425
  wait = wait_str.lower() in {"true", "yes", "1"}
427
426
  action_str = request.query_params.get("action", "interrupt")
428
- action = action_str if action_str in {"interrupt", "rollback"} else "interrupt"
427
+ action = cast(
428
+ Literal["interrupt", "rollback"],
429
+ action_str if action_str in {"interrupt", "rollback"} else "interrupt",
430
+ )
429
431
 
430
432
  async with connect() as conn:
431
433
  await Runs.cancel(
@@ -471,8 +473,9 @@ async def cancel_runs(
471
473
  for rid in run_ids:
472
474
  validate_uuid(rid, "Invalid run ID: must be a UUID")
473
475
  action_str = request.query_params.get("action", "interrupt")
474
- action: Literal["interrupt", "rollback"] = (
475
- action_str if action_str in ("interrupt", "rollback") else "interrupt"
476
+ action = cast(
477
+ Literal["interrupt", "rollback"],
478
+ action_str if action_str in ("interrupt", "rollback") else "interrupt",
476
479
  )
477
480
 
478
481
  async with connect() as conn:
@@ -56,6 +56,8 @@ async def handle_ui(request: ApiRequest) -> Response:
56
56
 
57
57
  # Use http:// protocol if accessing a localhost service
58
58
  def is_host(needle: str) -> bool:
59
+ if not isinstance(host, str):
60
+ return False
59
61
  return host.startswith(needle + ":") or host == needle
60
62
 
61
63
  protocol = "http:" if is_host("localhost") or is_host("127.0.0.1") else ""
@@ -13,7 +13,7 @@ from httpx import AsyncByteStream, Request, Response
13
13
  if typing.TYPE_CHECKING: # pragma: no cover
14
14
  import asyncio
15
15
 
16
- import trio
16
+ import trio # type: ignore[unresolved-import]
17
17
 
18
18
  Event = asyncio.Event | trio.Event
19
19
 
@@ -37,7 +37,7 @@ def is_running_trio() -> bool:
37
37
 
38
38
  def create_event() -> Event:
39
39
  if is_running_trio():
40
- import trio
40
+ import trio # type: ignore[unresolved-import]
41
41
 
42
42
  return trio.Event()
43
43
 
@@ -119,7 +119,7 @@ def create_task(
119
119
 
120
120
  def run_coroutine_threadsafe(
121
121
  coro: Coroutine[Any, Any, T], ignore_exceptions: tuple[type[Exception], ...] = ()
122
- ) -> concurrent.futures.Future[T | None]:
122
+ ) -> concurrent.futures.Future[T] | concurrent.futures.Future[None]:
123
123
  if _MAIN_LOOP is None:
124
124
  raise RuntimeError("No event loop set")
125
125
  future = asyncio.run_coroutine_threadsafe(coro, _MAIN_LOOP)
@@ -226,7 +226,7 @@ def to_aiter(*args: T) -> AsyncIterator[T]:
226
226
  V = TypeVar("V")
227
227
 
228
228
 
229
- class aclosing(Generic[V], AbstractAsyncContextManager):
229
+ class aclosing(Generic[V], AbstractAsyncContextManager[V]):
230
230
  """Async context manager for safely finalizing an asynchronously cleaned-up
231
231
  resource such as an async generator, calling its ``aclose()`` method.
232
232
 
@@ -255,14 +255,16 @@ class aclosing(Generic[V], AbstractAsyncContextManager):
255
255
  await self.thing.aclose()
256
256
 
257
257
 
258
- async def aclosing_aiter(aiter: AsyncIterator[T]) -> AsyncIterator[T]:
259
- if hasattr(aiter, "__aenter__"):
260
- async with aiter:
261
- async for item in aiter:
258
+ async def aclosing_aiter(
259
+ aiterator: AsyncIterator[T],
260
+ ) -> AsyncIterator[T]:
261
+ if hasattr(aiterator, "__aenter__"):
262
+ async with aiterator: # type: ignore[invalid-context-manager]
263
+ async for item in aiterator:
262
264
  yield item
263
265
  else:
264
- async with aclosing(aiter):
265
- async for item in aiter:
266
+ async with aclosing(aiterator):
267
+ async for item in aiterator:
266
268
  yield item
267
269
 
268
270