langgraph-agent-common 2.0.1__tar.gz → 2.0.3__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 (37) hide show
  1. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/PKG-INFO +1 -1
  2. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/pyproject.toml +1 -1
  3. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/adaptor/agent_executor.py +16 -16
  4. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/langgraph/validator.py +4 -1
  5. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/resources/static/css/style.css +137 -49
  6. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/resources/static/index.html +7 -24
  7. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/resources/static/js/app.js +209 -104
  8. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_agent_common.egg-info/PKG-INFO +1 -1
  9. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/README.md +0 -0
  10. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/setup.cfg +0 -0
  11. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/__init__.py +0 -0
  12. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/adaptor/__init__.py +0 -0
  13. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/adaptor/conversion.py +0 -0
  14. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/adaptor/sanitize_messages_for_llm.py +0 -0
  15. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/fastapi_application.py +0 -0
  16. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/langgraph/__init__.py +0 -0
  17. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/langgraph/checkpointer.py +0 -0
  18. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/langgraph/graph.py +0 -0
  19. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/langgraph/store.py +0 -0
  20. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/resources/openapi_schema.json +0 -0
  21. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/server.py +0 -0
  22. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/tasks/__init__.py +0 -0
  23. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/tasks/push_notification_config_store.py +0 -0
  24. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/tasks/task_store.py +0 -0
  25. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/utils/__init__.py +0 -0
  26. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/utils/lock.py +0 -0
  27. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_a2a_api/utils/types.py +0 -0
  28. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_agent_common.egg-info/SOURCES.txt +0 -0
  29. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_agent_common.egg-info/dependency_links.txt +0 -0
  30. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_agent_common.egg-info/requires.txt +0 -0
  31. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_agent_common.egg-info/top_level.txt +0 -0
  32. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_aux/__init__.py +0 -0
  33. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_aux/callbacks/__init__.py +0 -0
  34. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_aux/callbacks/talos_callback_handler.py +0 -0
  35. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_aux/utils/__init__.py +0 -0
  36. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/src/langgraph_aux/utils/stream_output.py +0 -0
  37. {langgraph_agent_common-2.0.1 → langgraph_agent_common-2.0.3}/test/test_deepagents_a2a_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-agent-common
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: langgraph-agent-common
5
5
  Author-email: Aegon <1006785434@qq.com>
6
6
  Project-URL: repository, https://github.com/liuxiahuiyi/langgraph-agent-common.git
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "langgraph-agent-common"
3
- version = "2.0.1"
3
+ version = "2.0.3"
4
4
  description = "langgraph-agent-common"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Aegon", email = "1006785434@qq.com" }]
@@ -92,13 +92,8 @@ class GraphAgentExecutor(AgentExecutor):
92
92
  if current_interrupts:
93
93
  current_interrupt_ids = [c.id for c in current_interrupts]
94
94
  if isinstance(langgraph_message.content, list) and len(current_interrupts) == len(langgraph_message.content) and all([isinstance(part, dict) and part.get("type") == "non_standard" and validate_HITLResponse(part.get("value")) for part in langgraph_message.content]):
95
- decisions = {part.get("value", {}).get("hitl_id"): part.get("value", {}).get("decision") for part in langgraph_message.content}
96
- if set(current_interrupt_ids) == set(decisions.keys()):
97
- resume_decisions = []
98
- for hitl_id in current_interrupt_ids:
99
- resume_decisions.append(decisions[hitl_id])
100
- resume_content = {"decisions": resume_decisions}
101
- else:
95
+ resume_content = {part.get("value", {}).get("hitl_id"): {"decisions": part.get("value", {}).get("decisions")} for part in langgraph_message.content}
96
+ if set(current_interrupt_ids) != set(resume_content.keys()):
102
97
  raise ValueError(f"Interrupt resume content: {langgraph_message.content}. Must contain overall original decision ids {current_interrupt_ids}.")
103
98
  else:
104
99
  raise ValueError(f"Invalid interrupt resume content: {langgraph_message.content}. Must be a list with the same length as interrupt ids: {current_interrupt_ids}. Each item in the list must be a data with HITLResponse schema: {HITLResponseModel.schema()}.")
@@ -124,7 +119,9 @@ class GraphAgentExecutor(AgentExecutor):
124
119
  finally:
125
120
  self.run_controls.pop(task_id, None)
126
121
 
127
- async def _get_interrupts(self, state: StateSnapshot, subgraphs: bool, seen_ids: set[str] = set()) -> list[Interrupt]:
122
+ async def _get_interrupts(self, state: StateSnapshot, subgraphs: bool, seen_ids: set[str] = None) -> list[Interrupt]:
123
+ if seen_ids is None:
124
+ seen_ids = set()
128
125
  interrupts = []
129
126
  for interrupt in state.interrupts:
130
127
  if interrupt.id not in seen_ids:
@@ -235,11 +232,6 @@ class GraphAgentExecutor(AgentExecutor):
235
232
  yield pre_yield
236
233
  pre_yield = None
237
234
  if step == "__interrupt__":
238
- graph_status = TASK_STATE_INPUT_REQUIRED
239
- pre_yield = {
240
- "messages": [AIMessage(content=any_content_to_langgraph([{"hitl_id": interrupt.id, "content": interrupt.value} for interrupt in update]))],
241
- "graph_status": graph_status,
242
- }
243
235
  break_out = True
244
236
  else:
245
237
  messages = self._normalize_messages(update.get("messages"))
@@ -256,7 +248,16 @@ class GraphAgentExecutor(AgentExecutor):
256
248
  "graph_status": graph_status,
257
249
  }
258
250
  else:
259
- yield pre_yield
251
+ current_state_after = await self.graph_with_ck.aget_state(config, subgraphs=subgraphs)
252
+ current_interrupts_after = await self._get_interrupts(current_state_after, subgraphs)
253
+ if not current_interrupts_after:
254
+ raise RuntimeError(f"__interrupt__ step encountered but no pending interrupts found in state.")
255
+ graph_status = TASK_STATE_INPUT_REQUIRED
256
+ yield {
257
+ "messages": [AIMessage(content=any_content_to_langgraph([{"hitl_id": interrupt.id, "content": interrupt.value} for interrupt in current_interrupts_after]))],
258
+ "graph_status": graph_status,
259
+ }
260
+
260
261
 
261
262
  except GraphRecursionError as e:
262
263
  if pre_yield is not None:
@@ -343,8 +344,7 @@ class GraphAgentExecutor(AgentExecutor):
343
344
  if alive_task.status.state == TASK_STATE_INPUT_REQUIRED:
344
345
  if alive_task.id == task_id:
345
346
  await updater.start_work()
346
- history_tasks = await self.task_store.get_rejected_tasks(context_id)
347
- return (True, history_tasks)
347
+ return (True, [])
348
348
  else:
349
349
  return (False, [alive_task])
350
350
  else:
@@ -27,11 +27,14 @@ class RespondDecision(BaseModel):
27
27
  message: str
28
28
 
29
29
 
30
+ Decision = ApproveDecision | EditDecision | RejectDecision | RespondDecision
31
+
32
+
30
33
  class HITLResponseModel(BaseModel):
31
34
  model_config = ConfigDict(extra="forbid")
32
35
 
33
36
  hitl_id: str
34
- decision: ApproveDecision | EditDecision | RejectDecision | RespondDecision
37
+ decisions: list[Decision]
35
38
 
36
39
 
37
40
  def validate_HITLResponse(payload: Any) -> bool:
@@ -310,68 +310,172 @@ textarea:focus {
310
310
  color: var(--warning);
311
311
  }
312
312
 
313
- .hitl-content {
313
+ .hitl-header-actions {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 8px;
317
+ }
318
+
319
+ .hitl-progress-label {
320
+ font-size: 12px;
321
+ color: var(--text-secondary);
322
+ }
323
+
324
+ .hitl-cards-container {
314
325
  display: flex;
315
326
  flex-direction: column;
316
327
  gap: 12px;
317
328
  }
318
329
 
319
- .hitl-section h5 {
320
- font-size: 13px;
330
+ .hitl-card {
331
+ background: var(--bg-tertiary);
332
+ border: 1px solid var(--border);
333
+ border-radius: 8px;
334
+ padding: 12px;
335
+ }
336
+
337
+ .hitl-card-header {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 10px;
341
+ margin-bottom: 10px;
342
+ }
343
+
344
+ .hitl-card-title {
345
+ font-size: 14px;
346
+ font-weight: 600;
347
+ color: var(--warning);
348
+ }
349
+
350
+ .hitl-card-id {
351
+ font-size: 11px;
352
+ font-family: monospace;
321
353
  color: var(--text-secondary);
322
- margin-bottom: 8px;
354
+ background: var(--bg-primary);
355
+ padding: 2px 6px;
356
+ border-radius: 3px;
323
357
  }
324
358
 
325
- .json-display {
359
+ .hitl-card-status {
360
+ margin-left: auto;
361
+ font-size: 12px;
362
+ color: var(--text-secondary);
363
+ }
364
+
365
+ .hitl-card-status.decided {
366
+ color: var(--success);
367
+ }
368
+
369
+ .hitl-card-actions {
370
+ margin-bottom: 10px;
371
+ }
372
+
373
+ .hitl-card-actions h6 {
374
+ font-size: 12px;
375
+ color: var(--text-secondary);
376
+ margin-bottom: 6px;
377
+ }
378
+
379
+ .hitl-action-block {
380
+ padding: 10px 0;
381
+ border-bottom: 1px solid var(--border);
382
+ }
383
+
384
+ .hitl-action-block:last-child {
385
+ border-bottom: none;
386
+ }
387
+
388
+ .hitl-action-item {
326
389
  background: var(--bg-primary);
327
- padding: 12px;
328
390
  border-radius: 4px;
391
+ padding: 8px;
392
+ margin-bottom: 6px;
393
+ }
394
+
395
+ .hitl-action-name {
396
+ font-size: 13px;
397
+ font-weight: 500;
398
+ color: var(--accent);
399
+ }
400
+
401
+ .hitl-action-args {
329
402
  font-family: 'Monaco', 'Menlo', monospace;
330
- font-size: 12px;
331
- max-height: 200px;
332
- overflow: auto;
403
+ font-size: 11px;
404
+ color: var(--text-secondary);
405
+ margin-top: 4px;
333
406
  white-space: pre-wrap;
334
407
  word-break: break-word;
335
408
  }
336
409
 
337
- .hitl-decisions {
410
+ .hitl-card-decisions {
338
411
  display: flex;
339
412
  gap: 8px;
340
413
  margin-bottom: 8px;
414
+ flex-wrap: wrap;
341
415
  }
342
416
 
343
- .hitl-nav {
344
- display: flex;
345
- align-items: center;
346
- justify-content: space-between;
347
- margin-bottom: 12px;
348
- padding: 8px;
349
- background: var(--bg-primary);
350
- border-radius: 4px;
417
+ .hitl-card-form {
418
+ margin-top: 8px;
351
419
  }
352
420
 
353
- .hitl-nav-btn {
354
- padding: 6px 12px;
355
- background: var(--bg-tertiary);
421
+ .hitl-card-form.hidden {
422
+ display: none;
423
+ }
424
+
425
+ .hitl-card-input {
426
+ width: 100%;
427
+ padding: 8px;
428
+ background: var(--bg-secondary);
356
429
  border: 1px solid var(--border);
357
430
  border-radius: 4px;
358
431
  color: var(--text-primary);
359
- cursor: pointer;
432
+ font-family: 'Monaco', 'Menlo', monospace;
360
433
  font-size: 12px;
434
+ resize: vertical;
361
435
  }
362
436
 
363
- .hitl-nav-btn:hover:not(:disabled) {
364
- background: var(--border);
437
+ .hitl-card-edit {
438
+ margin-top: 8px;
365
439
  }
366
440
 
367
- .hitl-nav-btn:disabled {
368
- opacity: 0.5;
369
- cursor: not-allowed;
441
+ .hitl-card-edit.hidden {
442
+ display: none;
370
443
  }
371
444
 
372
- .hitl-nav-info {
373
- font-size: 13px;
445
+ .hitl-card-edit h6 {
446
+ font-size: 12px;
374
447
  color: var(--text-secondary);
448
+ margin-bottom: 4px;
449
+ }
450
+
451
+ .hitl-card-edit-input {
452
+ width: 100%;
453
+ padding: 8px;
454
+ background: var(--bg-secondary);
455
+ border: 1px solid var(--border);
456
+ border-radius: 4px;
457
+ color: var(--text-primary);
458
+ font-family: 'Monaco', 'Menlo', monospace;
459
+ font-size: 12px;
460
+ resize: vertical;
461
+ }
462
+
463
+ .json-display {
464
+ background: var(--bg-primary);
465
+ padding: 12px;
466
+ border-radius: 4px;
467
+ font-family: 'Monaco', 'Menlo', monospace;
468
+ font-size: 12px;
469
+ max-height: 200px;
470
+ overflow: auto;
471
+ white-space: pre-wrap;
472
+ word-break: break-word;
473
+ }
474
+
475
+ .hitl-decisions {
476
+ display: flex;
477
+ gap: 8px;
478
+ margin-bottom: 8px;
375
479
  }
376
480
 
377
481
  .hitl-btn {
@@ -395,6 +499,10 @@ textarea:focus {
395
499
  background: var(--accent);
396
500
  }
397
501
 
502
+ .hitl-btn.edit-btn {
503
+ background: var(--warning);
504
+ }
505
+
398
506
  .hitl-btn:hover {
399
507
  opacity: 0.9;
400
508
  }
@@ -403,27 +511,7 @@ textarea:focus {
403
511
  box-shadow: 0 0 0 2px white;
404
512
  }
405
513
 
406
- #hitlResponseForm {
407
- margin-top: 8px;
408
- }
409
-
410
- #hitlResponseForm.hidden {
411
- display: none;
412
- }
413
-
414
- #hitlEditSection {
415
- margin-top: 8px;
416
- }
417
-
418
- #hitlEditSection.hidden {
419
- display: none;
420
- }
421
514
 
422
- #hitlEditSection h6 {
423
- font-size: 12px;
424
- color: var(--text-secondary);
425
- margin-bottom: 4px;
426
- }
427
515
 
428
516
  .chat-input-container {
429
517
  padding: 12px 16px;
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>A2A Chat UI</title>
7
- <link rel="stylesheet" href="static/css/style.css?v=5">
7
+ <link rel="stylesheet" href="static/css/style.css?v=6">
8
8
  </head>
9
9
  <body>
10
10
  <div class="app-container">
@@ -69,30 +69,13 @@
69
69
  <div id="hitlPanel" class="hitl-panel hidden">
70
70
  <div class="hitl-header">
71
71
  <h4>Input Required (HITL)</h4>
72
- <button id="closeHitlBtn" class="btn-small">Close</button>
73
- </div>
74
- <div class="hitl-content">
75
- <div class="hitl-section">
76
- <h5>Agent Response Parts:</h5>
77
- <pre id="hitlPartsJson" class="json-display"></pre>
78
- </div>
79
- <div class="hitl-section">
80
- <h5>HITL Response:</h5>
81
- <div class="hitl-decisions">
82
- <button class="hitl-btn approve-btn" data-type="approve">Approve</button>
83
- <button class="hitl-btn reject-btn" data-type="reject">Reject</button>
84
- <button class="hitl-btn respond-btn" data-type="respond">Respond</button>
85
- </div>
86
- <div id="hitlResponseForm" class="hidden">
87
- <textarea id="hitlResponseInput" rows="3" placeholder="Enter response message..."></textarea>
88
- <div id="hitlEditSection" class="hidden">
89
- <h6>Edited Action (JSON):</h6>
90
- <textarea id="hitlEditInput" rows="4" placeholder='{"name": "tool_name", "args": {}}'></textarea>
91
- </div>
92
- </div>
93
- <button id="submitHitlBtn" class="btn-primary hidden">Submit Response</button>
72
+ <div class="hitl-header-actions">
73
+ <span id="hitlProgressLabel" class="hitl-progress-label"></span>
74
+ <button id="submitAllHitlBtn" class="btn-primary" disabled>Submit All</button>
75
+ <button id="closeHitlBtn" class="btn-small">Close</button>
94
76
  </div>
95
77
  </div>
78
+ <div id="hitlCardsContainer" class="hitl-cards-container"></div>
96
79
  </div>
97
80
 
98
81
  <div class="chat-input-container">
@@ -102,6 +85,6 @@
102
85
  </main>
103
86
  </div>
104
87
 
105
- <script src="static/js/app.js?v=5"></script>
88
+ <script src="static/js/app.js?v=6"></script>
106
89
  </body>
107
90
  </html>
@@ -7,7 +7,7 @@
7
7
  currentTaskId: null,
8
8
  isSending: false,
9
9
  hitlState: null,
10
- selectedHitlType: null,
10
+ hitlDecisions: {},
11
11
  abortController: null,
12
12
  };
13
13
 
@@ -42,12 +42,9 @@
42
42
  elements.clearChatBtn = document.getElementById('clearChatBtn');
43
43
  elements.exportChatBtn = document.getElementById('exportChatBtn');
44
44
  elements.hitlPanel = document.getElementById('hitlPanel');
45
- elements.hitlPartsJson = document.getElementById('hitlPartsJson');
46
- elements.hitlResponseForm = document.getElementById('hitlResponseForm');
47
- elements.hitlResponseInput = document.getElementById('hitlResponseInput');
48
- elements.hitlEditSection = document.getElementById('hitlEditSection');
49
- elements.hitlEditInput = document.getElementById('hitlEditInput');
50
- elements.submitHitlBtn = document.getElementById('submitHitlBtn');
45
+ elements.hitlCardsContainer = document.getElementById('hitlCardsContainer');
46
+ elements.submitAllHitlBtn = document.getElementById('submitAllHitlBtn');
47
+ elements.hitlProgressLabel = document.getElementById('hitlProgressLabel');
51
48
  elements.closeHitlBtn = document.getElementById('closeHitlBtn');
52
49
  }
53
50
 
@@ -67,11 +64,7 @@
67
64
  elements.clearChatBtn.addEventListener('click', clearChat);
68
65
  elements.exportChatBtn.addEventListener('click', exportChat);
69
66
  elements.closeHitlBtn.addEventListener('click', () => elements.hitlPanel.classList.add('hidden'));
70
-
71
- document.querySelectorAll('.hitl-btn').forEach(btn => {
72
- btn.addEventListener('click', () => selectHitlType(btn.dataset.type));
73
- });
74
- elements.submitHitlBtn.addEventListener('click', submitHitlResponse);
67
+ elements.submitAllHitlBtn.addEventListener('click', submitAllHitlResponses);
75
68
  }
76
69
 
77
70
  function setApiBaseUrl() {
@@ -116,6 +109,7 @@
116
109
  renderChatHistory();
117
110
  elements.hitlPanel.classList.add('hidden');
118
111
  state.hitlState = null;
112
+ state.hitlDecisions = {};
119
113
  }
120
114
  }
121
115
 
@@ -232,6 +226,7 @@
232
226
  removeLoadingMessage();
233
227
  elements.hitlPanel.classList.add('hidden');
234
228
  state.hitlState = null;
229
+ state.hitlDecisions = {};
235
230
  state.isSending = false;
236
231
  state.currentTaskId = null;
237
232
  const context = getCurrentContext();
@@ -433,6 +428,7 @@
433
428
  context.taskId = null;
434
429
  elements.hitlPanel.classList.add('hidden');
435
430
  state.hitlState = null;
431
+ state.hitlDecisions = {};
436
432
  }
437
433
 
438
434
  updateContextDisplay();
@@ -449,137 +445,243 @@
449
445
 
450
446
  function showHitlPanel(task) {
451
447
  state.hitlState = task;
452
- state.selectedHitlType = null;
448
+ state.hitlDecisions = {}; // Map: hitl_id -> decision object
453
449
 
454
450
  const parts = task.status?.message?.parts || task.artifacts?.flatMap(a => a.parts) || [];
455
451
  const interruptParts = parts.filter(p => p.data && p.data.hitl_id);
456
452
 
457
453
  state.hitlState.interruptParts = interruptParts;
458
- state.hitlState.currentInterruptIndex = 0;
459
454
 
460
- elements.hitlPartsJson.textContent = JSON.stringify(parts, null, 2);
455
+ elements.hitlCardsContainer.innerHTML = '';
461
456
 
462
- if (interruptParts.length > 0) {
463
- updateInterruptDisplay();
457
+ if (interruptParts.length === 0) {
458
+ // Fallback: show raw parts JSON
459
+ const fallbackCard = document.createElement('div');
460
+ fallbackCard.className = 'hitl-card';
461
+ fallbackCard.innerHTML = `
462
+ <div class="hitl-card-header"><span class="hitl-card-title">Raw Parts</span></div>
463
+ <pre class="json-display">${JSON.stringify(parts, null, 2)}</pre>
464
+ `;
465
+ elements.hitlCardsContainer.appendChild(fallbackCard);
466
+ } else {
467
+ interruptParts.forEach((part, index) => {
468
+ const card = createHitlCard(part, index);
469
+ elements.hitlCardsContainer.appendChild(card);
470
+ });
464
471
  }
465
472
 
473
+ updateHitlProgress();
466
474
  elements.hitlPanel.classList.remove('hidden');
467
- elements.hitlResponseForm.classList.add('hidden');
468
- elements.submitHitlBtn.classList.add('hidden');
469
- elements.hitlEditSection.classList.add('hidden');
470
-
471
- document.querySelectorAll('.hitl-btn').forEach(btn => btn.classList.remove('active'));
472
475
  }
473
476
 
474
- function updateInterruptDisplay() {
475
- const interruptParts = state.hitlState.interruptParts;
476
- const currentIndex = state.hitlState.currentInterruptIndex;
477
+ function createHitlCard(part, index) {
478
+ const hitlId = part.data.hitl_id;
479
+ const content = part.data.content || {};
480
+ const actionRequests = content.action_requests || [];
481
+ const reviewConfigs = content.review_configs || [];
477
482
 
478
- if (interruptParts.length === 0) return;
483
+ const card = document.createElement('div');
484
+ card.className = 'hitl-card';
485
+ card.dataset.hitlId = hitlId;
486
+ card.dataset.index = index;
479
487
 
480
- const currentPart = interruptParts[currentIndex];
481
- const hitlId = currentPart.data.hitl_id;
482
- const interruptValue = currentPart.data.content;
488
+ const header = document.createElement('div');
489
+ header.className = 'hitl-card-header';
490
+ header.innerHTML = `
491
+ <span class="hitl-card-title">HITL #${index + 1}</span>
492
+ <span class="hitl-card-id">${hitlId}</span>
493
+ <span class="hitl-card-status" id="cardStatus_${index}">⏳ 0/${actionRequests.length || 1}</span>
494
+ `;
495
+ card.appendChild(header);
483
496
 
484
- let interruptInfo = `Interrupt ${currentIndex + 1}/${interruptParts.length}\n`;
485
- interruptInfo += `HITL ID: ${hitlId}\n\n`;
486
- interruptInfo += JSON.stringify(interruptValue, null, 2);
497
+ if (actionRequests.length === 0) {
498
+ const allowedDecisions = ['approve', 'reject', 'respond'];
499
+ const actionBlock = createActionBlock(hitlId, index, 0, null, allowedDecisions);
500
+ card.appendChild(actionBlock);
501
+ } else {
502
+ actionRequests.forEach((action, actionIdx) => {
503
+ const config = reviewConfigs[actionIdx] || reviewConfigs[0] || {};
504
+ const allowedDecisions = config.allowed_decisions || ['approve', 'reject', 'respond'];
505
+ const actionBlock = createActionBlock(hitlId, index, actionIdx, action, allowedDecisions);
506
+ card.appendChild(actionBlock);
507
+ });
508
+ }
487
509
 
488
- elements.hitlPartsJson.textContent = interruptInfo;
510
+ return card;
511
+ }
489
512
 
490
- const navHtml = `
491
- <div class="hitl-nav">
492
- <button class="hitl-nav-btn" id="prevInterruptBtn" ${currentIndex === 0 ? 'disabled' : ''}>Previous</button>
493
- <span class="hitl-nav-info">${currentIndex + 1} of ${interruptParts.length}</span>
494
- <button class="hitl-nav-btn" id="nextInterruptBtn" ${currentIndex === interruptParts.length - 1 ? 'disabled' : ''}>Next</button>
495
- </div>
496
- `;
513
+ function createActionBlock(hitlId, cardIndex, actionIdx, action, allowedDecisions) {
514
+ const blockId = `${cardIndex}_${actionIdx}`;
515
+ const block = document.createElement('div');
516
+ block.className = 'hitl-action-block';
497
517
 
498
- const existingNav = elements.hitlPanel.querySelector('.hitl-nav');
499
- if (existingNav) {
500
- existingNav.outerHTML = navHtml;
501
- } else {
502
- const hitlContent = elements.hitlPanel.querySelector('.hitl-content');
503
- hitlContent.insertAdjacentHTML('afterbegin', navHtml);
518
+ if (action) {
519
+ block.innerHTML += `
520
+ <div class="hitl-action-item">
521
+ <span class="hitl-action-name">${action.name}</span>
522
+ <pre class="hitl-action-args">${JSON.stringify(action.args, null, 2)}</pre>
523
+ </div>
524
+ `;
504
525
  }
505
526
 
506
- document.getElementById('prevInterruptBtn')?.addEventListener('click', () => {
507
- if (state.hitlState.currentInterruptIndex > 0) {
508
- state.hitlState.currentInterruptIndex--;
509
- updateInterruptDisplay();
510
- }
527
+ const decisionsDiv = document.createElement('div');
528
+ decisionsDiv.className = 'hitl-card-decisions';
529
+ allowedDecisions.forEach(type => {
530
+ const btn = document.createElement('button');
531
+ btn.className = `hitl-btn ${type}-btn`;
532
+ btn.dataset.type = type;
533
+ btn.dataset.blockId = blockId;
534
+ btn.textContent = type.charAt(0).toUpperCase() + type.slice(1);
535
+ btn.addEventListener('click', () => selectActionDecision(hitlId, cardIndex, actionIdx, type, block));
536
+ decisionsDiv.appendChild(btn);
511
537
  });
538
+ block.appendChild(decisionsDiv);
539
+
540
+ const formDiv = document.createElement('div');
541
+ formDiv.className = 'hitl-card-form hidden';
542
+ formDiv.id = `actionForm_${blockId}`;
543
+ formDiv.innerHTML = `
544
+ <textarea class="hitl-card-input" id="actionInput_${blockId}" rows="2" placeholder="Enter message..."></textarea>
545
+ <div class="hitl-card-edit hidden" id="actionEdit_${blockId}">
546
+ <h6>Edited Action (JSON):</h6>
547
+ <textarea class="hitl-card-edit-input" id="actionEditInput_${blockId}" rows="3" placeholder='{"name": "tool_name", "args": {}}'></textarea>
548
+ </div>
549
+ `;
550
+ block.appendChild(formDiv);
512
551
 
513
- document.getElementById('nextInterruptBtn')?.addEventListener('click', () => {
514
- if (state.hitlState.currentInterruptIndex < interruptParts.length - 1) {
515
- state.hitlState.currentInterruptIndex++;
516
- updateInterruptDisplay();
517
- }
518
- });
552
+ return block;
519
553
  }
520
554
 
521
- function selectHitlType(type) {
522
- state.selectedHitlType = type;
523
- document.querySelectorAll('.hitl-btn').forEach(btn => {
555
+ function selectActionDecision(hitlId, cardIndex, actionIdx, type, block) {
556
+ block.querySelectorAll('.hitl-btn').forEach(btn => {
524
557
  btn.classList.toggle('active', btn.dataset.type === type);
525
558
  });
526
559
 
527
- elements.hitlResponseForm.classList.remove('hidden');
528
- elements.submitHitlBtn.classList.remove('hidden');
560
+ const blockId = `${cardIndex}_${actionIdx}`;
561
+ const form = document.getElementById(`actionForm_${blockId}`);
562
+ const input = document.getElementById(`actionInput_${blockId}`);
563
+ const editSection = document.getElementById(`actionEdit_${blockId}`);
529
564
 
530
565
  if (type === 'approve') {
531
- elements.hitlResponseInput.value = '';
532
- elements.hitlResponseInput.placeholder = 'Optional message for approval';
533
- elements.hitlEditSection.classList.add('hidden');
566
+ form.classList.add('hidden');
567
+ editSection.classList.add('hidden');
534
568
  } else if (type === 'reject') {
535
- elements.hitlResponseInput.placeholder = 'Rejection reason...';
536
- elements.hitlEditSection.classList.add('hidden');
569
+ form.classList.remove('hidden');
570
+ input.placeholder = 'Rejection reason (optional)...';
571
+ editSection.classList.add('hidden');
537
572
  } else if (type === 'respond') {
538
- elements.hitlResponseInput.placeholder = 'Response message...';
539
- elements.hitlEditSection.classList.add('hidden');
573
+ form.classList.remove('hidden');
574
+ input.placeholder = 'Response message...';
575
+ editSection.classList.add('hidden');
576
+ } else if (type === 'edit') {
577
+ form.classList.remove('hidden');
578
+ input.placeholder = 'Edit message (optional)...';
579
+ editSection.classList.remove('hidden');
580
+ }
581
+
582
+ if (!state.hitlDecisions[hitlId]) {
583
+ state.hitlDecisions[hitlId] = {};
584
+ }
585
+ state.hitlDecisions[hitlId][actionIdx] = { type, blockId };
586
+
587
+ const interruptPart = state.hitlState.interruptParts.find(p => p.data.hitl_id === hitlId);
588
+ const totalActions = (interruptPart.data.content?.action_requests || []).length || 1;
589
+ const decidedCount = Object.keys(state.hitlDecisions[hitlId]).length;
590
+ const statusEl = document.getElementById(`cardStatus_${cardIndex}`);
591
+ if (statusEl) {
592
+ if (decidedCount >= totalActions) {
593
+ statusEl.textContent = `✓ ${decidedCount}/${totalActions}`;
594
+ statusEl.className = 'hitl-card-status decided';
595
+ } else {
596
+ statusEl.textContent = `⏳ ${decidedCount}/${totalActions}`;
597
+ statusEl.className = 'hitl-card-status';
598
+ }
540
599
  }
600
+
601
+ updateHitlProgress();
541
602
  }
542
603
 
543
- function submitHitlResponse() {
544
- if (!state.selectedHitlType || !state.hitlState) return;
604
+ function updateHitlProgress() {
605
+ const interruptParts = state.hitlState?.interruptParts || [];
606
+ let totalActions = 0;
607
+ let decidedActions = 0;
608
+ interruptParts.forEach(p => {
609
+ const actions = (p.data.content?.action_requests || []).length || 1;
610
+ totalActions += actions;
611
+ const cardDecisions = state.hitlDecisions[p.data.hitl_id];
612
+ if (cardDecisions) {
613
+ decidedActions += Object.keys(cardDecisions).length;
614
+ }
615
+ });
545
616
 
546
- const message = elements.hitlResponseInput.value.trim();
547
- const interruptParts = state.hitlState.interruptParts || [];
617
+ elements.hitlProgressLabel.textContent = `${decidedActions}/${totalActions} decided`;
618
+ elements.submitAllHitlBtn.disabled = decidedActions < totalActions;
619
+ }
548
620
 
549
- if (interruptParts.length === 0) {
550
- addSystemMessage('No interrupt parts found');
551
- return;
552
- }
621
+ function submitAllHitlResponses() {
622
+ if (!state.hitlState) return;
553
623
 
554
- const currentIndex = state.hitlState.currentInterruptIndex || 0;
555
- const currentPart = interruptParts[currentIndex];
556
- const hitlId = currentPart.data.hitl_id;
624
+ const interruptParts = state.hitlState.interruptParts || [];
557
625
 
558
- let decision;
626
+ const responseParts = interruptParts.map((part, idx) => {
627
+ const hitlId = part.data.hitl_id;
628
+ const cardDecisions = state.hitlDecisions[hitlId] || {};
629
+ const actionRequests = part.data.content?.action_requests || [];
630
+ const totalActions = actionRequests.length || 1;
631
+
632
+ const decisions = [];
633
+ for (let actionIdx = 0; actionIdx < totalActions; actionIdx++) {
634
+ const decisionInfo = cardDecisions[actionIdx];
635
+ if (!decisionInfo) {
636
+ addSystemMessage(`Decision missing for HITL #${idx + 1}, action #${actionIdx + 1}`);
637
+ return null;
638
+ }
559
639
 
560
- if (state.selectedHitlType === 'approve') {
561
- decision = { type: 'approve' };
562
- } else if (state.selectedHitlType === 'reject') {
563
- decision = { type: 'reject' };
564
- if (message) {
565
- decision.message = message;
566
- }
567
- } else if (state.selectedHitlType === 'respond') {
568
- if (!message) {
569
- addSystemMessage('Response message is required');
570
- return;
640
+ let decision = { type: decisionInfo.type };
641
+ const blockId = decisionInfo.blockId;
642
+ const inputEl = document.getElementById(`actionInput_${blockId}`);
643
+ const editInputEl = document.getElementById(`actionEditInput_${blockId}`);
644
+ const message = inputEl ? inputEl.value.trim() : '';
645
+ const editJson = editInputEl ? editInputEl.value.trim() : '';
646
+
647
+ if (decisionInfo.type === 'reject' && message) {
648
+ decision.message = message;
649
+ } else if (decisionInfo.type === 'respond') {
650
+ if (!message) {
651
+ addSystemMessage(`Response message is required for HITL #${idx + 1}, action #${actionIdx + 1}`);
652
+ return null;
653
+ }
654
+ decision.message = message;
655
+ } else if (decisionInfo.type === 'edit') {
656
+ if (editJson) {
657
+ try {
658
+ decision.edited_action = JSON.parse(editJson);
659
+ } catch (e) {
660
+ addSystemMessage(`Invalid JSON in edited action for HITL #${idx + 1}, action #${actionIdx + 1}`);
661
+ return null;
662
+ }
663
+ } else {
664
+ addSystemMessage(`Edited action JSON is required for HITL #${idx + 1}, action #${actionIdx + 1}`);
665
+ return null;
666
+ }
667
+ }
668
+
669
+ decisions.push(decision);
571
670
  }
572
- decision = { type: 'respond', message: message };
573
- }
574
671
 
575
- const hitlResponse = {
576
- hitl_id: hitlId,
577
- decision: decision
578
- };
672
+ return {
673
+ data: {
674
+ hitl_id: hitlId,
675
+ decisions: decisions,
676
+ }
677
+ };
678
+ });
679
+
680
+ // Check if any part failed validation
681
+ if (responseParts.some(p => p === null)) return;
579
682
 
580
683
  const messageMeta = getJsonInput('messageMeta') || {};
581
684
  const config = getJsonInput('config') || {};
582
-
583
685
  const taskId = state.hitlState.id;
584
686
  const messageId = generateId();
585
687
 
@@ -589,20 +691,23 @@
589
691
  contextId: state.currentContextId,
590
692
  taskId: taskId,
591
693
  role: 'ROLE_USER',
592
- parts: [{
593
- data: hitlResponse,
594
- }],
694
+ parts: responseParts,
595
695
  metadata: messageMeta,
596
696
  },
597
697
  configuration: config,
598
698
  metadata: getJsonInput('taskMeta') || {},
599
699
  };
600
700
 
701
+ // Clean up HITL panel
601
702
  elements.hitlPanel.classList.add('hidden');
703
+ const decisionSummary = interruptParts.map(p => {
704
+ const d = state.hitlDecisions[p.data.hitl_id];
705
+ return `${p.data.hitl_id}: ${d.type}`;
706
+ }).join(', ');
602
707
  state.hitlState = null;
603
- state.selectedHitlType = null;
708
+ state.hitlDecisions = {};
604
709
 
605
- addUserMessage(`[HITL Response] HITL ID: ${hitlId}, ${JSON.stringify(decision)}`);
710
+ addUserMessage(`[HITL Response] ${decisionSummary}`);
606
711
  addLoadingMessage();
607
712
 
608
713
  state.isSending = true;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-agent-common
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: langgraph-agent-common
5
5
  Author-email: Aegon <1006785434@qq.com>
6
6
  Project-URL: repository, https://github.com/liuxiahuiyi/langgraph-agent-common.git