coco-framework 0.1.0__py3-none-any.whl

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 (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,461 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Callable, Iterator, Sequence
4
+ from dataclasses import fields, is_dataclass
5
+ from typing import (
6
+ Any,
7
+ TypeVar,
8
+ cast,
9
+ )
10
+ from uuid import UUID, uuid4
11
+
12
+ from coco_core.callbacks import BaseCallbackHandler
13
+ from coco_core.messages import BaseMessage, ToolMessage
14
+ from coco_core.messages.utils import convert_to_messages
15
+ from coco_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
16
+ from pydantic import BaseModel
17
+
18
+ from coco_graph._internal._config import filter_to_user_tags
19
+ from coco_graph._internal._constants import NS_SEP
20
+ from coco_graph.constants import TAG_HIDDEN, TAG_NOSTREAM
21
+ from coco_graph.pregel.protocol import StreamChunk
22
+ from coco_graph.types import Command
23
+
24
+ try:
25
+ from coco_core.tracers._streaming import _StreamingCallbackHandler
26
+ except ImportError:
27
+ _StreamingCallbackHandler = object # type: ignore
28
+
29
+ try:
30
+ from coco_core.tracers._streaming import _V2StreamingCallbackHandler
31
+ except ImportError:
32
+ _V2StreamingCallbackHandler = object # type: ignore
33
+
34
+ T = TypeVar("T")
35
+ Meta = tuple[tuple[str, ...], dict[str, Any]]
36
+
37
+
38
+ def _state_values(obj: Any) -> Sequence[Any]:
39
+ """Extract top-level field values from a state object (dict, BaseModel, or dataclass)."""
40
+ if isinstance(obj, dict):
41
+ return list(obj.values())
42
+ elif isinstance(obj, BaseModel):
43
+ return [getattr(obj, k) for k in type(obj).model_fields]
44
+ elif is_dataclass(obj) and not isinstance(obj, type):
45
+ return [getattr(obj, f.name) for f in fields(obj)]
46
+ return ()
47
+
48
+
49
+ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
50
+ """A callback handler that implements stream_mode=messages.
51
+
52
+ Collects messages from:
53
+ (1) chat model stream events; and
54
+ (2) node outputs.
55
+ """
56
+
57
+ run_inline = True
58
+ """We want this callback to run in the main thread to avoid order/locking issues."""
59
+
60
+ def __init__(
61
+ self,
62
+ stream: Callable[[StreamChunk], None],
63
+ subgraphs: bool,
64
+ *,
65
+ parent_ns: tuple[str, ...] | None = None,
66
+ ) -> None:
67
+ """Configure the handler to stream messages from LLMs and nodes.
68
+
69
+ Args:
70
+ stream: A callable that takes a StreamChunk and emits it.
71
+ subgraphs: Whether to emit messages from subgraphs.
72
+ parent_ns: The namespace where the handler was created.
73
+ We keep track of this namespace to allow calls to subgraphs that
74
+ were explicitly requested as a stream with `messages` mode
75
+ configured.
76
+
77
+ Example:
78
+ parent_ns is used to handle scenarios where the subgraph is explicitly
79
+ streamed with `stream_mode="messages"`.
80
+
81
+ ```python
82
+ def parent_graph_node():
83
+ # This node is in the parent graph.
84
+ async for event in some_subgraph(..., stream_mode="messages"):
85
+ do something with event # <-- these events will be emitted
86
+ return ...
87
+
88
+ parent_graph.invoke(subgraphs=False)
89
+ ```
90
+ """
91
+ self.stream = stream
92
+ self.subgraphs = subgraphs
93
+ self.metadata: dict[UUID, Meta] = {}
94
+ self.seen: set[int | str] = set()
95
+ self.parent_ns = parent_ns
96
+
97
+ def _emit(self, meta: Meta, message: BaseMessage, *, dedupe: bool = False) -> None:
98
+ if dedupe and message.id in self.seen:
99
+ return
100
+ else:
101
+ if message.id is None:
102
+ message.id = str(uuid4())
103
+ self.seen.add(message.id)
104
+ self.stream((meta[0], "messages", (message, meta[1])))
105
+
106
+ def _find_and_emit_messages(self, meta: Meta, response: Any) -> None:
107
+ if isinstance(response, BaseMessage):
108
+ self._emit(meta, response, dedupe=True)
109
+ elif isinstance(response, Sequence):
110
+ for value in response:
111
+ if isinstance(value, BaseMessage):
112
+ self._emit(meta, value, dedupe=True)
113
+ else:
114
+ for value in _state_values(response):
115
+ if isinstance(value, BaseMessage):
116
+ self._emit(meta, value, dedupe=True)
117
+ elif isinstance(value, Sequence):
118
+ for item in value:
119
+ if isinstance(item, BaseMessage):
120
+ self._emit(meta, item, dedupe=True)
121
+
122
+ def tap_output_aiter(
123
+ self, run_id: UUID, output: AsyncIterator[T]
124
+ ) -> AsyncIterator[T]:
125
+ return output
126
+
127
+ def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
128
+ return output
129
+
130
+ def on_chat_model_start(
131
+ self,
132
+ serialized: dict[str, Any],
133
+ messages: list[list[BaseMessage]],
134
+ *,
135
+ run_id: UUID,
136
+ parent_run_id: UUID | None = None,
137
+ tags: list[str] | None = None,
138
+ metadata: dict[str, Any] | None = None,
139
+ **kwargs: Any,
140
+ ) -> Any:
141
+ if metadata and (not tags or (TAG_NOSTREAM not in tags)):
142
+ ns = tuple(cast(str, metadata["coco_graph_checkpoint_ns"]).split(NS_SEP))[
143
+ :-1
144
+ ]
145
+ if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
146
+ return
147
+ if (filtered_tags := filter_to_user_tags(tags)) is not None:
148
+ metadata["tags"] = filtered_tags
149
+ self.metadata[run_id] = (ns, metadata)
150
+
151
+ def on_llm_new_token(
152
+ self,
153
+ token: str,
154
+ *,
155
+ chunk: ChatGenerationChunk | None = None,
156
+ run_id: UUID,
157
+ parent_run_id: UUID | None = None,
158
+ tags: list[str] | None = None,
159
+ **kwargs: Any,
160
+ ) -> Any:
161
+ if not isinstance(chunk, ChatGenerationChunk):
162
+ return
163
+ if meta := self.metadata.get(run_id):
164
+ self._emit(meta, chunk.message)
165
+
166
+ def on_llm_end(
167
+ self,
168
+ response: LLMResult,
169
+ *,
170
+ run_id: UUID,
171
+ parent_run_id: UUID | None = None,
172
+ **kwargs: Any,
173
+ ) -> Any:
174
+ if meta := self.metadata.get(run_id):
175
+ if response.generations and response.generations[0]:
176
+ gen = response.generations[0][0]
177
+ if isinstance(gen, ChatGeneration):
178
+ self._emit(meta, gen.message, dedupe=True)
179
+ self.metadata.pop(run_id, None)
180
+
181
+ def on_llm_error(
182
+ self,
183
+ error: BaseException,
184
+ *,
185
+ run_id: UUID,
186
+ parent_run_id: UUID | None = None,
187
+ **kwargs: Any,
188
+ ) -> Any:
189
+ self.metadata.pop(run_id, None)
190
+
191
+ def on_chain_start(
192
+ self,
193
+ serialized: dict[str, Any],
194
+ inputs: dict[str, Any],
195
+ *,
196
+ run_id: UUID,
197
+ parent_run_id: UUID | None = None,
198
+ tags: list[str] | None = None,
199
+ metadata: dict[str, Any] | None = None,
200
+ **kwargs: Any,
201
+ ) -> Any:
202
+ if (
203
+ metadata
204
+ and kwargs.get("name") == metadata.get("coco_graph_node")
205
+ and (not tags or TAG_HIDDEN not in tags)
206
+ ):
207
+ ns = tuple(cast(str, metadata["coco_graph_checkpoint_ns"]).split(NS_SEP))[
208
+ :-1
209
+ ]
210
+ if not self.subgraphs and len(ns) > 0:
211
+ return
212
+ self.metadata[run_id] = (ns, metadata)
213
+ for value in _state_values(inputs):
214
+ if isinstance(value, BaseMessage):
215
+ if value.id is not None:
216
+ self.seen.add(value.id)
217
+ elif isinstance(value, Sequence) and not isinstance(value, str):
218
+ for item in value:
219
+ if isinstance(item, BaseMessage):
220
+ if item.id is not None:
221
+ self.seen.add(item.id)
222
+
223
+ def on_chain_end(
224
+ self,
225
+ response: Any,
226
+ *,
227
+ run_id: UUID,
228
+ parent_run_id: UUID | None = None,
229
+ **kwargs: Any,
230
+ ) -> Any:
231
+ if meta := self.metadata.pop(run_id, None):
232
+ # Handle Command node updates
233
+ if isinstance(response, Command):
234
+ self._find_and_emit_messages(meta, response.update)
235
+ # Handle list of Command updates
236
+ elif isinstance(response, Sequence) and any(
237
+ isinstance(value, Command) for value in response
238
+ ):
239
+ for value in response:
240
+ if isinstance(value, Command):
241
+ self._find_and_emit_messages(meta, value.update)
242
+ else:
243
+ self._find_and_emit_messages(meta, value)
244
+ # Handle basic updates / streaming
245
+ else:
246
+ self._find_and_emit_messages(meta, response)
247
+
248
+ def on_chain_error(
249
+ self,
250
+ error: BaseException,
251
+ *,
252
+ run_id: UUID,
253
+ parent_run_id: UUID | None = None,
254
+ **kwargs: Any,
255
+ ) -> Any:
256
+ self.metadata.pop(run_id, None)
257
+
258
+
259
+ class StreamMessagesHandlerV2(StreamMessagesHandler, _V2StreamingCallbackHandler):
260
+ """v2 variant of `StreamMessagesHandler`.
261
+
262
+ Declaring `_V2StreamingCallbackHandler` as a base flips
263
+ `BaseChatModel.invoke` to route through `_stream_chat_model_events`
264
+ (firing `on_stream_event`) instead of `_stream` (firing
265
+ `on_llm_new_token`). Inherits `on_stream_event` from the parent,
266
+ which forwards protocol events onto the messages stream channel.
267
+
268
+ Pregel attaches this class instead of the v1 handler only when
269
+ `StreamingHandler` opts in via the internal
270
+ `CONFIG_KEY_STREAM_MESSAGES_V2` config key; direct
271
+ `graph.stream(stream_mode="messages")` callers keep the v1
272
+ AIMessageChunk shape.
273
+ """
274
+
275
+ def on_llm_new_token(
276
+ self,
277
+ token: str,
278
+ *,
279
+ chunk: ChatGenerationChunk | None = None,
280
+ run_id: UUID,
281
+ parent_run_id: UUID | None = None,
282
+ tags: list[str] | None = None,
283
+ **kwargs: Any,
284
+ ) -> Any:
285
+ """Intentional no-op — v1 chunks are not used on v2-flagged runs.
286
+
287
+ The v2 marker already steers `invoke` to the event generator, so
288
+ `on_llm_new_token` should not fire under normal routing. This
289
+ override stays a pass-through (no call to `super()`) to make
290
+ the intent explicit and to guard against any caller (e.g. a
291
+ node that calls `model.stream()` directly, which still fires
292
+ the v1 callback) leaking AIMessageChunks onto a v2-flagged
293
+ messages stream.
294
+ """
295
+ # Intentionally empty: v2 handler does not forward v1 chunks.
296
+
297
+ def __init__(
298
+ self,
299
+ stream: Callable[[StreamChunk], None],
300
+ subgraphs: bool,
301
+ *,
302
+ parent_ns: tuple[str, ...] | None = None,
303
+ ) -> None:
304
+ super().__init__(stream, subgraphs, parent_ns=parent_ns)
305
+ self._streamed_run_ids: set[UUID] = set()
306
+
307
+ def _find_and_emit_messages(self, meta: Meta, response: Any) -> None:
308
+ """Like the v1 handler, but skip ToolMessage from node outputs.
309
+
310
+ Tool results belong on the tools channel / state in v3; v2-flagged streams
311
+ must not replay finalized ToolMessages as chat tokens (see MessagesTransformer).
312
+ Legacy v1-only `stream_mode="messages"` still emits ToolMessages (see subgraph
313
+ streaming tests).
314
+ """
315
+ if isinstance(response, BaseMessage) and not isinstance(response, ToolMessage):
316
+ self._emit(meta, response, dedupe=True)
317
+ elif isinstance(response, Sequence):
318
+ for value in response:
319
+ if isinstance(value, BaseMessage) and not isinstance(
320
+ value, ToolMessage
321
+ ):
322
+ self._emit(meta, value, dedupe=True)
323
+ else:
324
+ for value in _state_values(response):
325
+ if isinstance(value, BaseMessage) and not isinstance(
326
+ value, ToolMessage
327
+ ):
328
+ self._emit(meta, value, dedupe=True)
329
+ elif isinstance(value, Sequence):
330
+ for item in value:
331
+ if isinstance(item, BaseMessage) and not isinstance(
332
+ item, ToolMessage
333
+ ):
334
+ self._emit(meta, item, dedupe=True)
335
+
336
+ def on_llm_end(
337
+ self,
338
+ response: LLMResult,
339
+ *,
340
+ run_id: UUID,
341
+ parent_run_id: UUID | None = None,
342
+ **kwargs: Any,
343
+ ) -> Any:
344
+ if meta := self.metadata.get(run_id):
345
+ if response.generations and response.generations[0]:
346
+ gen = response.generations[0][0]
347
+ if isinstance(gen, ChatGeneration):
348
+ if run_id in self._streamed_run_ids:
349
+ if gen.message.id is None:
350
+ gen.message.id = str(uuid4())
351
+ self.seen.add(gen.message.id)
352
+ else:
353
+ self._emit(meta, gen.message, dedupe=True)
354
+ self._streamed_run_ids.discard(run_id)
355
+ self.metadata.pop(run_id, None)
356
+
357
+ def on_llm_error(
358
+ self,
359
+ error: BaseException,
360
+ *,
361
+ run_id: UUID,
362
+ parent_run_id: UUID | None = None,
363
+ **kwargs: Any,
364
+ ) -> Any:
365
+ self._streamed_run_ids.discard(run_id)
366
+ super().on_llm_error(
367
+ error,
368
+ run_id=run_id,
369
+ parent_run_id=parent_run_id,
370
+ **kwargs,
371
+ )
372
+
373
+ def on_stream_event(
374
+ self,
375
+ event: dict[str, Any],
376
+ *,
377
+ run_id: UUID,
378
+ parent_run_id: UUID | None = None,
379
+ tags: list[str] | None = None,
380
+ **kwargs: Any,
381
+ ) -> Any:
382
+ """Forward a protocol event from `stream_events(version="v3")` as a messages stream part.
383
+
384
+ Fires once per `MessagesData` event (`message-start`, per-block
385
+ `content-block-*`, `message-finish`). The transformer layer
386
+ correlates events back to a single `ChatModelStream` via
387
+ `metadata["run_id"]` — attached here so the v1
388
+ `stream_mode="messages"` output (which emits
389
+ `(AIMessageChunk, metadata)` via `on_llm_new_token`) keeps its
390
+ original metadata shape.
391
+
392
+ Lives on the v2 handler rather than the v1 base: content-block
393
+ events are a v2-only concept, and forwarding them only when the
394
+ v2 handler is attached keeps the message channel's shape
395
+ predictable for v1 callers.
396
+ """
397
+ if meta := self.metadata.get(run_id):
398
+ # Record message_id on message-start so on_chain_end's
399
+ # dedupe skips the finalized AIMessage the node returns
400
+ # (otherwise the messages projection double-counts: once
401
+ # from streaming, once from the chain output).
402
+ if event.get("event") == "message-start":
403
+ self._streamed_run_ids.add(run_id)
404
+ msg_id = event.get("message_id")
405
+ if msg_id:
406
+ self.seen.add(msg_id)
407
+ v2_meta = {**meta[1], "run_id": str(run_id)}
408
+ self.stream((meta[0], "messages", (event, v2_meta)))
409
+
410
+
411
+ # Known role values (OpenAI-style) and type values (Coco serialisation)
412
+ # that identify a dict as a message. Checked before coercing to BaseMessage so
413
+ # we don't accidentally touch unrelated dicts that happen to have a "role" key.
414
+ _MESSAGE_ROLES: frozenset[str] = frozenset(
415
+ {"user", "human", "assistant", "ai", "tool", "system", "function"}
416
+ )
417
+ _MESSAGE_TYPES: frozenset[str] = frozenset(
418
+ {"human", "ai", "tool", "system", "function", "remove"}
419
+ )
420
+
421
+
422
+ def _is_message_dict(item: dict) -> bool:
423
+ return item.get("role") in _MESSAGE_ROLES or item.get("type") in _MESSAGE_TYPES
424
+
425
+
426
+ def ensure_message_ids(value: Any) -> None:
427
+ """Coerce message-like write values to typed BaseMessages with stable IDs.
428
+
429
+ Called in put_writes() before DeltaChannel writes are submitted to the
430
+ checkpointer. Without this the checkpoint may store raw dicts or id=None
431
+ BaseMessages; every get_state() replay then produces a different UUID and
432
+ the same message appears with a different ID in each LangSmith trace.
433
+
434
+ Handles three input shapes:
435
+ - BaseMessage objects: assign a UUID if id is None.
436
+ - Dicts with a known "role" (OpenAI-style) or "type" (Coco format) at
437
+ the root level: stamp "id" into the dict in-place. The reducer's
438
+ convert_to_messages call will forward the id to the resulting BaseMessage.
439
+ - Lists of the above: apply the same logic to each element, replacing dict
440
+ items with coerced BaseMessages so the shared list reference seen by
441
+ checkpoint_pending_writes and the background thread both get typed messages.
442
+
443
+ Mutating synchronously here (before the background thread is submitted) is
444
+ safe: the serialised bytes always reflect the post-coercion state.
445
+ """
446
+ if isinstance(value, BaseMessage):
447
+ if value.id is None:
448
+ value.id = str(uuid4())
449
+ elif isinstance(value, dict) and _is_message_dict(value):
450
+ if not value.get("id"):
451
+ value["id"] = str(uuid4())
452
+ elif isinstance(value, list):
453
+ for i, item in enumerate(value):
454
+ if isinstance(item, BaseMessage):
455
+ if item.id is None:
456
+ item.id = str(uuid4())
457
+ elif isinstance(item, dict) and _is_message_dict(item):
458
+ msg = convert_to_messages([item])[0]
459
+ if msg.id is None:
460
+ msg.id = str(uuid4())
461
+ value[i] = msg