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,1101 @@
1
+ """Internal tracer to power the event stream API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import logging
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ TypedDict,
12
+ TypeVar,
13
+ cast,
14
+ )
15
+
16
+ from typing_extensions import NotRequired, override
17
+
18
+ from coco_core.callbacks.base import AsyncCallbackHandler, BaseCallbackManager
19
+ from coco_core.messages import AIMessageChunk, BaseMessage, BaseMessageChunk
20
+ from coco_core.outputs import (
21
+ ChatGenerationChunk,
22
+ GenerationChunk,
23
+ LLMResult,
24
+ )
25
+ from coco_core.runnables import ensure_config
26
+ from coco_core.runnables.schema import (
27
+ CustomStreamEvent,
28
+ EventData,
29
+ StandardStreamEvent,
30
+ StreamEvent,
31
+ )
32
+ from coco_core.runnables.utils import (
33
+ Input,
34
+ Output,
35
+ _RootEventFilter,
36
+ )
37
+ from coco_core.tracers._streaming import _StreamingCallbackHandler
38
+ from coco_core.tracers.log_stream import (
39
+ LogStreamCallbackHandler,
40
+ RunLog,
41
+ _astream_log_implementation,
42
+ )
43
+ from coco_core.tracers.memory_stream import _get_or_create_loop, _MemoryStream
44
+ from coco_core.utils.aiter import aclosing
45
+ from coco_core.utils.uuid import uuid7
46
+
47
+ if TYPE_CHECKING:
48
+ from collections.abc import AsyncIterator, Iterator, Sequence
49
+ from uuid import UUID
50
+
51
+ from coco_core.documents import Document
52
+ from coco_core.runnables import Runnable, RunnableConfig
53
+ from coco_core.tracers.log_stream import LogEntry
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+
58
+ class RunInfo(TypedDict):
59
+ """Information about a run.
60
+
61
+ This is used to keep track of the metadata associated with a run.
62
+ """
63
+
64
+ name: str
65
+ """The name of the run."""
66
+
67
+ tags: list[str]
68
+ """The tags associated with the run."""
69
+
70
+ metadata: dict[str, Any]
71
+ """The metadata associated with the run."""
72
+
73
+ run_type: str
74
+ """The type of the run."""
75
+
76
+ inputs: NotRequired[Any]
77
+ """The inputs to the run."""
78
+
79
+ parent_run_id: UUID | None
80
+ """The ID of the parent run."""
81
+
82
+ tool_call_id: NotRequired[str | None]
83
+ """The tool call ID associated with the run."""
84
+
85
+
86
+ def _assign_name(name: str | None, serialized: dict[str, Any] | None) -> str:
87
+ """Assign a name to a run."""
88
+ if name is not None:
89
+ return name
90
+ if serialized is not None:
91
+ if "name" in serialized:
92
+ return cast("str", serialized["name"])
93
+ if "id" in serialized:
94
+ return cast("str", serialized["id"][-1])
95
+ return "Unnamed"
96
+
97
+
98
+ T = TypeVar("T")
99
+
100
+
101
+ class _AstreamEventsCallbackHandler(
102
+ AsyncCallbackHandler, _StreamingCallbackHandler[Any]
103
+ ):
104
+ """An implementation of an async callback handler for astream events."""
105
+
106
+ def __init__(
107
+ self,
108
+ *args: Any,
109
+ include_names: Sequence[str] | None = None,
110
+ include_types: Sequence[str] | None = None,
111
+ include_tags: Sequence[str] | None = None,
112
+ exclude_names: Sequence[str] | None = None,
113
+ exclude_types: Sequence[str] | None = None,
114
+ exclude_tags: Sequence[str] | None = None,
115
+ **kwargs: Any,
116
+ ) -> None:
117
+ """Initialize the tracer."""
118
+ super().__init__(*args, **kwargs)
119
+ # Map of run ID to run info.
120
+ # the entry corresponding to a given run id is cleaned
121
+ # up when each corresponding run ends.
122
+ self.run_map: dict[UUID, RunInfo] = {}
123
+ # The callback event that corresponds to the end of a parent run
124
+ # may be invoked BEFORE the callback event that corresponds to the end
125
+ # of a child run, which results in clean up of run_map.
126
+ # So we keep track of the mapping between children and parent run IDs
127
+ # in a separate container. This container is GCed when the tracer is GCed.
128
+ self.parent_map: dict[UUID, UUID | None] = {}
129
+
130
+ self.is_tapped: dict[UUID, Any] = {}
131
+
132
+ # Filter which events will be sent over the queue.
133
+ self.root_event_filter = _RootEventFilter(
134
+ include_names=include_names,
135
+ include_types=include_types,
136
+ include_tags=include_tags,
137
+ exclude_names=exclude_names,
138
+ exclude_types=exclude_types,
139
+ exclude_tags=exclude_tags,
140
+ )
141
+
142
+ memory_stream = _MemoryStream[StreamEvent](_get_or_create_loop(self))
143
+ self.send_stream = memory_stream.get_send_stream()
144
+ self.receive_stream = memory_stream.get_receive_stream()
145
+
146
+ def _get_parent_ids(self, run_id: UUID) -> list[str]:
147
+ """Get the parent IDs of a run (non-recursively) cast to strings."""
148
+ parent_ids = []
149
+
150
+ while parent_id := self.parent_map.get(run_id):
151
+ str_parent_id = str(parent_id)
152
+ if str_parent_id in parent_ids:
153
+ msg = (
154
+ f"Parent ID {parent_id} is already in the parent_ids list. "
155
+ f"This should never happen."
156
+ )
157
+ raise AssertionError(msg)
158
+ parent_ids.append(str_parent_id)
159
+ run_id = parent_id
160
+
161
+ # Return the parent IDs in reverse order, so that the first
162
+ # parent ID is the root and the last ID is the immediate parent.
163
+ return parent_ids[::-1]
164
+
165
+ def _send(self, event: StreamEvent, event_type: str) -> None:
166
+ """Send an event to the stream."""
167
+ if self.root_event_filter.include_event(event, event_type):
168
+ self.send_stream.send_nowait(event)
169
+
170
+ def __aiter__(self) -> AsyncIterator[Any]:
171
+ """Iterate over the receive stream.
172
+
173
+ Returns:
174
+ An async iterator over the receive stream.
175
+ """
176
+ return self.receive_stream.__aiter__()
177
+
178
+ async def tap_output_aiter(
179
+ self, run_id: UUID, output: AsyncIterator[T]
180
+ ) -> AsyncIterator[T]:
181
+ """Tap the output aiter.
182
+
183
+ This method is used to tap the output of a `Runnable` that produces an async
184
+ iterator. It is used to generate stream events for the output of the `Runnable`.
185
+
186
+ Args:
187
+ run_id: The ID of the run.
188
+ output: The output of the `Runnable`.
189
+
190
+ Yields:
191
+ The output of the `Runnable`.
192
+ """
193
+ sentinel = object()
194
+ # atomic check and set
195
+ tap = self.is_tapped.setdefault(run_id, sentinel)
196
+ # wait for first chunk
197
+ first = await anext(output, sentinel)
198
+ if first is sentinel:
199
+ return
200
+ # get run info
201
+ run_info = self.run_map.get(run_id)
202
+ if run_info is None:
203
+ # run has finished, don't issue any stream events
204
+ yield cast("T", first)
205
+ return
206
+ if tap is sentinel:
207
+ # if we are the first to tap, issue stream events
208
+ event: StandardStreamEvent = {
209
+ "event": f"on_{run_info['run_type']}_stream",
210
+ "run_id": str(run_id),
211
+ "name": run_info["name"],
212
+ "tags": run_info["tags"],
213
+ "metadata": run_info["metadata"],
214
+ "data": {},
215
+ "parent_ids": self._get_parent_ids(run_id),
216
+ }
217
+ self._send({**event, "data": {"chunk": first}}, run_info["run_type"])
218
+ yield cast("T", first)
219
+ # consume the rest of the output
220
+ async for chunk in output:
221
+ self._send(
222
+ {**event, "data": {"chunk": chunk}},
223
+ run_info["run_type"],
224
+ )
225
+ yield chunk
226
+ else:
227
+ # otherwise just pass through
228
+ yield cast("T", first)
229
+ # consume the rest of the output
230
+ async for chunk in output:
231
+ yield chunk
232
+
233
+ def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
234
+ """Tap the output iter.
235
+
236
+ Args:
237
+ run_id: The ID of the run.
238
+ output: The output of the `Runnable`.
239
+
240
+ Yields:
241
+ The output of the `Runnable`.
242
+ """
243
+ sentinel = object()
244
+ # atomic check and set
245
+ tap = self.is_tapped.setdefault(run_id, sentinel)
246
+ # wait for first chunk
247
+ first = next(output, sentinel)
248
+ if first is sentinel:
249
+ return
250
+ # get run info
251
+ run_info = self.run_map.get(run_id)
252
+ if run_info is None:
253
+ # run has finished, don't issue any stream events
254
+ yield cast("T", first)
255
+ return
256
+ if tap is sentinel:
257
+ # if we are the first to tap, issue stream events
258
+ event: StandardStreamEvent = {
259
+ "event": f"on_{run_info['run_type']}_stream",
260
+ "run_id": str(run_id),
261
+ "name": run_info["name"],
262
+ "tags": run_info["tags"],
263
+ "metadata": run_info["metadata"],
264
+ "data": {},
265
+ "parent_ids": self._get_parent_ids(run_id),
266
+ }
267
+ self._send({**event, "data": {"chunk": first}}, run_info["run_type"])
268
+ yield cast("T", first)
269
+ # consume the rest of the output
270
+ for chunk in output:
271
+ self._send(
272
+ {**event, "data": {"chunk": chunk}},
273
+ run_info["run_type"],
274
+ )
275
+ yield chunk
276
+ else:
277
+ # otherwise just pass through
278
+ yield cast("T", first)
279
+ # consume the rest of the output
280
+ for chunk in output:
281
+ yield chunk
282
+
283
+ def _write_run_start_info(
284
+ self,
285
+ run_id: UUID,
286
+ *,
287
+ tags: list[str] | None,
288
+ metadata: dict[str, Any] | None,
289
+ parent_run_id: UUID | None,
290
+ name_: str,
291
+ run_type: str,
292
+ **kwargs: Any,
293
+ ) -> None:
294
+ """Update the run info."""
295
+ info: RunInfo = {
296
+ "tags": tags or [],
297
+ "metadata": metadata or {},
298
+ "name": name_,
299
+ "run_type": run_type,
300
+ "parent_run_id": parent_run_id,
301
+ }
302
+
303
+ if "inputs" in kwargs:
304
+ # Handle inputs in a special case to allow inputs to be an
305
+ # optionally provided and distinguish between missing value
306
+ # vs. None value.
307
+ info["inputs"] = kwargs["inputs"]
308
+
309
+ if "tool_call_id" in kwargs:
310
+ # Store tool_call_id in run info for linking errors to tool calls
311
+ info["tool_call_id"] = kwargs["tool_call_id"]
312
+
313
+ self.run_map[run_id] = info
314
+ self.parent_map[run_id] = parent_run_id
315
+
316
+ @override
317
+ async def on_chat_model_start(
318
+ self,
319
+ serialized: dict[str, Any],
320
+ messages: list[list[BaseMessage]],
321
+ *,
322
+ run_id: UUID,
323
+ tags: list[str] | None = None,
324
+ parent_run_id: UUID | None = None,
325
+ metadata: dict[str, Any] | None = None,
326
+ name: str | None = None,
327
+ **kwargs: Any,
328
+ ) -> None:
329
+ """Start a trace for a chat model run."""
330
+ name_ = _assign_name(name, serialized)
331
+ run_type = "chat_model"
332
+
333
+ self._write_run_start_info(
334
+ run_id,
335
+ tags=tags,
336
+ metadata=metadata,
337
+ parent_run_id=parent_run_id,
338
+ name_=name_,
339
+ run_type=run_type,
340
+ inputs={"messages": messages},
341
+ )
342
+
343
+ self._send(
344
+ {
345
+ "event": "on_chat_model_start",
346
+ "data": {
347
+ "input": {"messages": messages},
348
+ },
349
+ "name": name_,
350
+ "tags": tags or [],
351
+ "run_id": str(run_id),
352
+ "metadata": metadata or {},
353
+ "parent_ids": self._get_parent_ids(run_id),
354
+ },
355
+ run_type,
356
+ )
357
+
358
+ @override
359
+ async def on_llm_start(
360
+ self,
361
+ serialized: dict[str, Any],
362
+ prompts: list[str],
363
+ *,
364
+ run_id: UUID,
365
+ tags: list[str] | None = None,
366
+ parent_run_id: UUID | None = None,
367
+ metadata: dict[str, Any] | None = None,
368
+ name: str | None = None,
369
+ **kwargs: Any,
370
+ ) -> None:
371
+ """Start a trace for a (non-chat model) LLM run."""
372
+ name_ = _assign_name(name, serialized)
373
+ run_type = "llm"
374
+
375
+ self._write_run_start_info(
376
+ run_id,
377
+ tags=tags,
378
+ metadata=metadata,
379
+ parent_run_id=parent_run_id,
380
+ name_=name_,
381
+ run_type=run_type,
382
+ inputs={"prompts": prompts},
383
+ )
384
+
385
+ self._send(
386
+ {
387
+ "event": "on_llm_start",
388
+ "data": {
389
+ "input": {
390
+ "prompts": prompts,
391
+ }
392
+ },
393
+ "name": name_,
394
+ "tags": tags or [],
395
+ "run_id": str(run_id),
396
+ "metadata": metadata or {},
397
+ "parent_ids": self._get_parent_ids(run_id),
398
+ },
399
+ run_type,
400
+ )
401
+
402
+ @override
403
+ async def on_custom_event(
404
+ self,
405
+ name: str,
406
+ data: Any,
407
+ *,
408
+ run_id: UUID,
409
+ tags: list[str] | None = None,
410
+ metadata: dict[str, Any] | None = None,
411
+ **kwargs: Any,
412
+ ) -> None:
413
+ """Generate a custom astream event."""
414
+ event = CustomStreamEvent(
415
+ event="on_custom_event",
416
+ run_id=str(run_id),
417
+ name=name,
418
+ tags=tags or [],
419
+ metadata=metadata or {},
420
+ data=data,
421
+ parent_ids=self._get_parent_ids(run_id),
422
+ )
423
+ self._send(event, name)
424
+
425
+ @override
426
+ async def on_llm_new_token(
427
+ self,
428
+ token: str | list[str | dict[str, Any]],
429
+ *,
430
+ chunk: GenerationChunk | ChatGenerationChunk | None = None,
431
+ run_id: UUID,
432
+ parent_run_id: UUID | None = None,
433
+ **kwargs: Any,
434
+ ) -> None:
435
+ """Run on new output token.
436
+
437
+ Only available when streaming is enabled.
438
+
439
+ For both chat models and non-chat models (legacy text-completion LLMs).
440
+
441
+ Raises:
442
+ ValueError: If the run type is not `llm` or `chat_model`.
443
+ AssertionError: If the run ID is not found in the run map.
444
+ """
445
+ run_info = self.run_map.get(run_id)
446
+ chunk_: GenerationChunk | BaseMessageChunk
447
+
448
+ if run_info is None:
449
+ msg = f"Run ID {run_id} not found in run map."
450
+ raise AssertionError(msg)
451
+ if self.is_tapped.get(run_id):
452
+ return
453
+ if run_info["run_type"] == "chat_model":
454
+ event = "on_chat_model_stream"
455
+
456
+ if chunk is None:
457
+ chunk_ = AIMessageChunk(content=token)
458
+ else:
459
+ chunk_ = cast("ChatGenerationChunk", chunk).message
460
+
461
+ elif run_info["run_type"] == "llm":
462
+ event = "on_llm_stream"
463
+ if chunk is None:
464
+ text = token if isinstance(token, str) else ""
465
+ chunk_ = GenerationChunk(text=text)
466
+ else:
467
+ chunk_ = cast("GenerationChunk", chunk)
468
+ else:
469
+ msg = f"Unexpected run type: {run_info['run_type']}"
470
+ raise ValueError(msg)
471
+
472
+ self._send(
473
+ {
474
+ "event": event,
475
+ "data": {
476
+ "chunk": chunk_,
477
+ },
478
+ "run_id": str(run_id),
479
+ "name": run_info["name"],
480
+ "tags": run_info["tags"],
481
+ "metadata": run_info["metadata"],
482
+ "parent_ids": self._get_parent_ids(run_id),
483
+ },
484
+ run_info["run_type"],
485
+ )
486
+
487
+ @override
488
+ async def on_llm_end(
489
+ self, response: LLMResult, *, run_id: UUID, **kwargs: Any
490
+ ) -> None:
491
+ """End a trace for a model run.
492
+
493
+ For both chat models and non-chat models (legacy text-completion LLMs).
494
+
495
+ Raises:
496
+ ValueError: If the run type is not `'llm'` or `'chat_model'`.
497
+ """
498
+ run_info = self.run_map.pop(run_id)
499
+ inputs_ = run_info.get("inputs")
500
+
501
+ generations: list[list[GenerationChunk]] | list[list[ChatGenerationChunk]]
502
+ output: dict[str, Any] | BaseMessage = {}
503
+
504
+ if run_info["run_type"] == "chat_model":
505
+ generations = cast("list[list[ChatGenerationChunk]]", response.generations)
506
+ for gen in generations:
507
+ if output != {}:
508
+ break
509
+ for chunk in gen:
510
+ output = chunk.message
511
+ break
512
+
513
+ event = "on_chat_model_end"
514
+ elif run_info["run_type"] == "llm":
515
+ generations = cast("list[list[GenerationChunk]]", response.generations)
516
+ output = {
517
+ "generations": [
518
+ [
519
+ {
520
+ "text": chunk.text,
521
+ "generation_info": chunk.generation_info,
522
+ "type": chunk.type,
523
+ }
524
+ for chunk in gen
525
+ ]
526
+ for gen in generations
527
+ ],
528
+ "llm_output": response.llm_output,
529
+ }
530
+ event = "on_llm_end"
531
+ else:
532
+ msg = f"Unexpected run type: {run_info['run_type']}"
533
+ raise ValueError(msg)
534
+
535
+ self._send(
536
+ {
537
+ "event": event,
538
+ "data": {"output": output, "input": inputs_},
539
+ "run_id": str(run_id),
540
+ "name": run_info["name"],
541
+ "tags": run_info["tags"],
542
+ "metadata": run_info["metadata"],
543
+ "parent_ids": self._get_parent_ids(run_id),
544
+ },
545
+ run_info["run_type"],
546
+ )
547
+
548
+ async def on_chain_start(
549
+ self,
550
+ serialized: dict[str, Any],
551
+ inputs: dict[str, Any],
552
+ *,
553
+ run_id: UUID,
554
+ tags: list[str] | None = None,
555
+ parent_run_id: UUID | None = None,
556
+ metadata: dict[str, Any] | None = None,
557
+ run_type: str | None = None,
558
+ name: str | None = None,
559
+ **kwargs: Any,
560
+ ) -> None:
561
+ """Start a trace for a chain run."""
562
+ name_ = _assign_name(name, serialized)
563
+ run_type_ = run_type or "chain"
564
+
565
+ data: EventData = {}
566
+
567
+ # Work-around Runnable core code not sending input in some
568
+ # cases.
569
+ if inputs != {"input": ""}:
570
+ data["input"] = inputs
571
+ kwargs["inputs"] = inputs
572
+
573
+ self._write_run_start_info(
574
+ run_id,
575
+ tags=tags,
576
+ metadata=metadata,
577
+ parent_run_id=parent_run_id,
578
+ name_=name_,
579
+ run_type=run_type_,
580
+ **kwargs,
581
+ )
582
+
583
+ self._send(
584
+ {
585
+ "event": f"on_{run_type_}_start",
586
+ "data": data,
587
+ "name": name_,
588
+ "tags": tags or [],
589
+ "run_id": str(run_id),
590
+ "metadata": metadata or {},
591
+ "parent_ids": self._get_parent_ids(run_id),
592
+ },
593
+ run_type_,
594
+ )
595
+
596
+ @override
597
+ async def on_chain_end(
598
+ self,
599
+ outputs: dict[str, Any],
600
+ *,
601
+ run_id: UUID,
602
+ inputs: dict[str, Any] | None = None,
603
+ **kwargs: Any,
604
+ ) -> None:
605
+ """End a trace for a chain run."""
606
+ run_info = self.run_map.pop(run_id)
607
+ run_type = run_info["run_type"]
608
+
609
+ event = f"on_{run_type}_end"
610
+
611
+ inputs = inputs or run_info.get("inputs") or {}
612
+
613
+ data: EventData = {
614
+ "output": outputs,
615
+ "input": inputs,
616
+ }
617
+
618
+ self._send(
619
+ {
620
+ "event": event,
621
+ "data": data,
622
+ "run_id": str(run_id),
623
+ "name": run_info["name"],
624
+ "tags": run_info["tags"],
625
+ "metadata": run_info["metadata"],
626
+ "parent_ids": self._get_parent_ids(run_id),
627
+ },
628
+ run_type,
629
+ )
630
+
631
+ def _get_tool_run_info_with_inputs(self, run_id: UUID) -> tuple[RunInfo, Any]:
632
+ """Get run info for a tool and extract inputs, with validation.
633
+
634
+ Args:
635
+ run_id: The run ID of the tool.
636
+
637
+ Returns:
638
+ A tuple of `(run_info, inputs)`.
639
+
640
+ Raises:
641
+ AssertionError: If the run ID is a tool call and does not have inputs.
642
+ """
643
+ run_info = self.run_map.pop(run_id)
644
+ if "inputs" not in run_info:
645
+ msg = (
646
+ f"Run ID {run_id} is a tool call and is expected to have "
647
+ f"inputs associated with it."
648
+ )
649
+ raise AssertionError(msg)
650
+ inputs = run_info["inputs"]
651
+ return run_info, inputs
652
+
653
+ @override
654
+ async def on_tool_start(
655
+ self,
656
+ serialized: dict[str, Any],
657
+ input_str: str,
658
+ *,
659
+ run_id: UUID,
660
+ tags: list[str] | None = None,
661
+ parent_run_id: UUID | None = None,
662
+ metadata: dict[str, Any] | None = None,
663
+ name: str | None = None,
664
+ inputs: dict[str, Any] | None = None,
665
+ **kwargs: Any,
666
+ ) -> None:
667
+ """Start a trace for a tool run."""
668
+ name_ = _assign_name(name, serialized)
669
+
670
+ self._write_run_start_info(
671
+ run_id,
672
+ tags=tags,
673
+ metadata=metadata,
674
+ parent_run_id=parent_run_id,
675
+ name_=name_,
676
+ run_type="tool",
677
+ inputs=inputs,
678
+ tool_call_id=kwargs.get("tool_call_id"),
679
+ )
680
+
681
+ self._send(
682
+ {
683
+ "event": "on_tool_start",
684
+ "data": {
685
+ "input": inputs or {},
686
+ },
687
+ "name": name_,
688
+ "tags": tags or [],
689
+ "run_id": str(run_id),
690
+ "metadata": metadata or {},
691
+ "parent_ids": self._get_parent_ids(run_id),
692
+ },
693
+ "tool",
694
+ )
695
+
696
+ @override
697
+ async def on_tool_error(
698
+ self,
699
+ error: BaseException,
700
+ *,
701
+ run_id: UUID,
702
+ parent_run_id: UUID | None = None,
703
+ tags: list[str] | None = None,
704
+ **kwargs: Any,
705
+ ) -> None:
706
+ """Run when tool errors."""
707
+ # Extract tool_call_id from kwargs if passed directly, or from run_info
708
+ # (which was stored during on_tool_start) as a fallback
709
+ tool_call_id = kwargs.get("tool_call_id")
710
+ run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
711
+ if tool_call_id is None:
712
+ tool_call_id = run_info.get("tool_call_id")
713
+
714
+ event: StandardStreamEvent = {
715
+ "event": "on_tool_error",
716
+ "data": {
717
+ "error": error,
718
+ "input": inputs,
719
+ "tool_call_id": tool_call_id,
720
+ },
721
+ "run_id": str(run_id),
722
+ "name": run_info["name"],
723
+ "tags": run_info["tags"],
724
+ "metadata": run_info["metadata"],
725
+ "parent_ids": self._get_parent_ids(run_id),
726
+ }
727
+ self._send(event, "tool")
728
+
729
+ @override
730
+ async def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> None:
731
+ """End a trace for a tool run."""
732
+ run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
733
+
734
+ self._send(
735
+ {
736
+ "event": "on_tool_end",
737
+ "data": {
738
+ "output": output,
739
+ "input": inputs,
740
+ },
741
+ "run_id": str(run_id),
742
+ "name": run_info["name"],
743
+ "tags": run_info["tags"],
744
+ "metadata": run_info["metadata"],
745
+ "parent_ids": self._get_parent_ids(run_id),
746
+ },
747
+ "tool",
748
+ )
749
+
750
+ @override
751
+ async def on_retriever_start(
752
+ self,
753
+ serialized: dict[str, Any],
754
+ query: str,
755
+ *,
756
+ run_id: UUID,
757
+ parent_run_id: UUID | None = None,
758
+ tags: list[str] | None = None,
759
+ metadata: dict[str, Any] | None = None,
760
+ name: str | None = None,
761
+ **kwargs: Any,
762
+ ) -> None:
763
+ """Run when `Retriever` starts running."""
764
+ name_ = _assign_name(name, serialized)
765
+ run_type = "retriever"
766
+
767
+ self._write_run_start_info(
768
+ run_id,
769
+ tags=tags,
770
+ metadata=metadata,
771
+ parent_run_id=parent_run_id,
772
+ name_=name_,
773
+ run_type=run_type,
774
+ inputs={"query": query},
775
+ )
776
+
777
+ self._send(
778
+ {
779
+ "event": "on_retriever_start",
780
+ "data": {
781
+ "input": {
782
+ "query": query,
783
+ }
784
+ },
785
+ "name": name_,
786
+ "tags": tags or [],
787
+ "run_id": str(run_id),
788
+ "metadata": metadata or {},
789
+ "parent_ids": self._get_parent_ids(run_id),
790
+ },
791
+ run_type,
792
+ )
793
+
794
+ @override
795
+ async def on_retriever_end(
796
+ self, documents: Sequence[Document], *, run_id: UUID, **kwargs: Any
797
+ ) -> None:
798
+ """Run when `Retriever` ends running."""
799
+ run_info = self.run_map.pop(run_id)
800
+
801
+ self._send(
802
+ {
803
+ "event": "on_retriever_end",
804
+ "data": {
805
+ "output": documents,
806
+ "input": run_info.get("inputs"),
807
+ },
808
+ "run_id": str(run_id),
809
+ "name": run_info["name"],
810
+ "tags": run_info["tags"],
811
+ "metadata": run_info["metadata"],
812
+ "parent_ids": self._get_parent_ids(run_id),
813
+ },
814
+ run_info["run_type"],
815
+ )
816
+
817
+ def __deepcopy__(
818
+ self, memo: dict[int, Any] | None = None
819
+ ) -> _AstreamEventsCallbackHandler:
820
+ """Return self."""
821
+ return self
822
+
823
+ def __copy__(self) -> _AstreamEventsCallbackHandler:
824
+ """Return self."""
825
+ return self
826
+
827
+
828
+ async def _astream_events_implementation_v1(
829
+ runnable: Runnable[Input, Output],
830
+ value: Any,
831
+ config: RunnableConfig | None = None,
832
+ *,
833
+ include_names: Sequence[str] | None = None,
834
+ include_types: Sequence[str] | None = None,
835
+ include_tags: Sequence[str] | None = None,
836
+ exclude_names: Sequence[str] | None = None,
837
+ exclude_types: Sequence[str] | None = None,
838
+ exclude_tags: Sequence[str] | None = None,
839
+ **kwargs: Any,
840
+ ) -> AsyncIterator[StandardStreamEvent]:
841
+ stream = LogStreamCallbackHandler(
842
+ auto_close=False,
843
+ include_names=include_names,
844
+ include_types=include_types,
845
+ include_tags=include_tags,
846
+ exclude_names=exclude_names,
847
+ exclude_types=exclude_types,
848
+ exclude_tags=exclude_tags,
849
+ _schema_format="streaming_events",
850
+ )
851
+
852
+ run_log = RunLog(state=None) # type: ignore[arg-type]
853
+ encountered_start_event = False
854
+
855
+ root_event_filter = _RootEventFilter(
856
+ include_names=include_names,
857
+ include_types=include_types,
858
+ include_tags=include_tags,
859
+ exclude_names=exclude_names,
860
+ exclude_types=exclude_types,
861
+ exclude_tags=exclude_tags,
862
+ )
863
+
864
+ config = ensure_config(config)
865
+ root_tags = config.get("tags", [])
866
+ root_metadata = config.get("metadata", {})
867
+ root_name = config.get("run_name", runnable.get_name())
868
+
869
+ async for log in _astream_log_implementation(
870
+ runnable,
871
+ value,
872
+ config=config,
873
+ stream=stream,
874
+ diff=True,
875
+ with_streamed_output_list=True,
876
+ **kwargs,
877
+ ):
878
+ run_log += log
879
+
880
+ if not encountered_start_event:
881
+ # Yield the start event for the root runnable.
882
+ encountered_start_event = True
883
+ state = run_log.state.copy()
884
+
885
+ event = StandardStreamEvent(
886
+ event=f"on_{state['type']}_start",
887
+ run_id=state["id"],
888
+ name=root_name,
889
+ tags=root_tags,
890
+ metadata=root_metadata,
891
+ data={
892
+ "input": value,
893
+ },
894
+ parent_ids=[], # Not supported in v1
895
+ )
896
+
897
+ if root_event_filter.include_event(event, state["type"]):
898
+ yield event
899
+
900
+ paths = {
901
+ op["path"].split("/")[2]
902
+ for op in log.ops
903
+ if op["path"].startswith("/logs/")
904
+ }
905
+ # Elements in a set should be iterated in the same order
906
+ # as they were inserted in modern python versions.
907
+ for path in paths:
908
+ data: EventData = {}
909
+ log_entry: LogEntry = run_log.state["logs"][path]
910
+ if log_entry["end_time"] is None:
911
+ event_type = "stream" if log_entry["streamed_output"] else "start"
912
+ else:
913
+ event_type = "end"
914
+
915
+ if event_type == "start":
916
+ # Include the inputs with the start event if they are available.
917
+ # Usually they will NOT be available for components that operate
918
+ # on streams, since those components stream the input and
919
+ # don't know its final value until the end of the stream.
920
+ inputs = log_entry.get("inputs")
921
+ if inputs is not None:
922
+ data["input"] = inputs
923
+
924
+ if event_type == "end":
925
+ inputs = log_entry.get("inputs")
926
+ if inputs is not None:
927
+ data["input"] = inputs
928
+
929
+ # None is a VALID output for an end event
930
+ data["output"] = log_entry["final_output"]
931
+
932
+ if event_type == "stream":
933
+ num_chunks = len(log_entry["streamed_output"])
934
+ if num_chunks != 1:
935
+ msg = (
936
+ f"Expected exactly one chunk of streamed output, "
937
+ f"got {num_chunks} instead. This is impossible. "
938
+ f"Encountered in: {log_entry['name']}"
939
+ )
940
+ raise AssertionError(msg)
941
+
942
+ data = {"chunk": log_entry["streamed_output"][0]}
943
+ # Clean up the stream, we don't need it anymore.
944
+ # And this avoids duplicates as well!
945
+ log_entry["streamed_output"] = []
946
+
947
+ yield StandardStreamEvent(
948
+ event=f"on_{log_entry['type']}_{event_type}",
949
+ name=log_entry["name"],
950
+ run_id=log_entry["id"],
951
+ tags=log_entry["tags"],
952
+ metadata=log_entry["metadata"],
953
+ data=data,
954
+ parent_ids=[], # Not supported in v1
955
+ )
956
+
957
+ # Finally, we take care of the streaming output from the root chain
958
+ # if there is any.
959
+ state = run_log.state
960
+ if state["streamed_output"]:
961
+ num_chunks = len(state["streamed_output"])
962
+ if num_chunks != 1:
963
+ msg = (
964
+ f"Expected exactly one chunk of streamed output, "
965
+ f"got {num_chunks} instead. This is impossible. "
966
+ f"Encountered in: {state['name']}"
967
+ )
968
+ raise AssertionError(msg)
969
+
970
+ data = {"chunk": state["streamed_output"][0]}
971
+ # Clean up the stream, we don't need it anymore.
972
+ state["streamed_output"] = []
973
+
974
+ event = StandardStreamEvent(
975
+ event=f"on_{state['type']}_stream",
976
+ run_id=state["id"],
977
+ tags=root_tags,
978
+ metadata=root_metadata,
979
+ name=root_name,
980
+ data=data,
981
+ parent_ids=[], # Not supported in v1
982
+ )
983
+ if root_event_filter.include_event(event, state["type"]):
984
+ yield event
985
+
986
+ state = run_log.state
987
+
988
+ # Finally yield the end event for the root runnable.
989
+ event = StandardStreamEvent(
990
+ event=f"on_{state['type']}_end",
991
+ name=root_name,
992
+ run_id=state["id"],
993
+ tags=root_tags,
994
+ metadata=root_metadata,
995
+ data={
996
+ "output": state["final_output"],
997
+ },
998
+ parent_ids=[], # Not supported in v1
999
+ )
1000
+ if root_event_filter.include_event(event, state["type"]):
1001
+ yield event
1002
+
1003
+
1004
+ async def _astream_events_implementation_v2(
1005
+ runnable: Runnable[Input, Output],
1006
+ value: Any,
1007
+ config: RunnableConfig | None = None,
1008
+ *,
1009
+ include_names: Sequence[str] | None = None,
1010
+ include_types: Sequence[str] | None = None,
1011
+ include_tags: Sequence[str] | None = None,
1012
+ exclude_names: Sequence[str] | None = None,
1013
+ exclude_types: Sequence[str] | None = None,
1014
+ exclude_tags: Sequence[str] | None = None,
1015
+ **kwargs: Any,
1016
+ ) -> AsyncIterator[StandardStreamEvent]:
1017
+ """Implementation of the astream events API for v2 runnables."""
1018
+ event_streamer = _AstreamEventsCallbackHandler(
1019
+ include_names=include_names,
1020
+ include_types=include_types,
1021
+ include_tags=include_tags,
1022
+ exclude_names=exclude_names,
1023
+ exclude_types=exclude_types,
1024
+ exclude_tags=exclude_tags,
1025
+ )
1026
+
1027
+ # Assign the stream handler to the config
1028
+ config = ensure_config(config)
1029
+ if "run_id" in config:
1030
+ run_id = cast("UUID", config["run_id"])
1031
+ else:
1032
+ run_id = uuid7()
1033
+ config["run_id"] = run_id
1034
+ callbacks = config.get("callbacks")
1035
+ if callbacks is None:
1036
+ config["callbacks"] = [event_streamer]
1037
+ elif isinstance(callbacks, list):
1038
+ config["callbacks"] = [*callbacks, event_streamer]
1039
+ elif isinstance(callbacks, BaseCallbackManager):
1040
+ callbacks = callbacks.copy()
1041
+ callbacks.add_handler(event_streamer, inherit=True)
1042
+ config["callbacks"] = callbacks
1043
+ else:
1044
+ msg = ( # type: ignore[unreachable]
1045
+ f"Unexpected type for callbacks: {callbacks}."
1046
+ "Expected None, list or AsyncCallbackManager."
1047
+ )
1048
+ raise ValueError(msg)
1049
+
1050
+ # Call the runnable in streaming mode,
1051
+ # add each chunk to the output stream
1052
+ async def consume_astream() -> None:
1053
+ try:
1054
+ # if astream also calls tap_output_aiter this will be a no-op
1055
+ async with aclosing(runnable.astream(value, config, **kwargs)) as stream:
1056
+ async for _ in event_streamer.tap_output_aiter(run_id, stream):
1057
+ # All the content will be picked up
1058
+ pass
1059
+ finally:
1060
+ await event_streamer.send_stream.aclose()
1061
+
1062
+ # Start the runnable in a task, so we can start consuming output
1063
+ task = asyncio.create_task(consume_astream())
1064
+
1065
+ first_event_sent = False
1066
+ first_event_run_id = None
1067
+
1068
+ try:
1069
+ async for event in event_streamer:
1070
+ if not first_event_sent:
1071
+ first_event_sent = True
1072
+ # This is a work-around an issue where the inputs into the
1073
+ # chain are not available until the entire input is consumed.
1074
+ # As a temporary solution, we'll modify the input to be the input
1075
+ # that was passed into the chain.
1076
+ event["data"]["input"] = value
1077
+ first_event_run_id = event["run_id"]
1078
+ yield event
1079
+ continue
1080
+
1081
+ # If it's the end event corresponding to the root runnable
1082
+ # we don't include the input in the event since it's guaranteed
1083
+ # to be included in the first event.
1084
+ if (
1085
+ event["run_id"] == first_event_run_id
1086
+ and event["event"].endswith("_end")
1087
+ and "input" in event["data"]
1088
+ ):
1089
+ del event["data"]["input"]
1090
+
1091
+ yield event
1092
+ except asyncio.CancelledError as exc:
1093
+ # Cancel the task if it's still running
1094
+ task.cancel(exc.args[0] if exc.args else None)
1095
+ raise
1096
+ finally:
1097
+ # Cancel the task if it's still running
1098
+ task.cancel()
1099
+ # Await it anyway, to run any cleanup code, and propagate any exceptions
1100
+ with contextlib.suppress(asyncio.CancelledError):
1101
+ await task