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,175 @@
1
+ """Base classes for output parsers that can handle streaming input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ )
9
+
10
+ from typing_extensions import override
11
+
12
+ from coco_core.messages import BaseMessage, BaseMessageChunk
13
+ from coco_core.output_parsers.base import BaseOutputParser, T
14
+ from coco_core.outputs import (
15
+ ChatGeneration,
16
+ ChatGenerationChunk,
17
+ Generation,
18
+ GenerationChunk,
19
+ )
20
+ from coco_core.runnables.config import run_in_executor
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import AsyncIterator, Iterator
24
+
25
+ from coco_core.runnables import RunnableConfig
26
+
27
+
28
+ class BaseTransformOutputParser(BaseOutputParser[T]):
29
+ """Base class for an output parser that can handle streaming input."""
30
+
31
+ def _transform(
32
+ self,
33
+ input: Iterator[str | BaseMessage],
34
+ ) -> Iterator[T]:
35
+ for chunk in input:
36
+ if isinstance(chunk, BaseMessage):
37
+ yield self.parse_result([ChatGeneration(message=chunk)])
38
+ else:
39
+ yield self.parse_result([Generation(text=chunk)])
40
+
41
+ async def _atransform(
42
+ self,
43
+ input: AsyncIterator[str | BaseMessage],
44
+ ) -> AsyncIterator[T]:
45
+ async for chunk in input:
46
+ if isinstance(chunk, BaseMessage):
47
+ yield await run_in_executor(
48
+ None, self.parse_result, [ChatGeneration(message=chunk)]
49
+ )
50
+ else:
51
+ yield await run_in_executor(
52
+ None, self.parse_result, [Generation(text=chunk)]
53
+ )
54
+
55
+ @override
56
+ def transform(
57
+ self,
58
+ input: Iterator[str | BaseMessage],
59
+ config: RunnableConfig | None = None,
60
+ **kwargs: Any,
61
+ ) -> Iterator[T]:
62
+ """Transform the input into the output format.
63
+
64
+ Args:
65
+ input: The input to transform.
66
+ config: The configuration to use for the transformation.
67
+ **kwargs: Additional keyword arguments.
68
+
69
+ Yields:
70
+ The transformed output.
71
+ """
72
+ yield from self._transform_stream_with_config(
73
+ input, self._transform, config, run_type="parser"
74
+ )
75
+
76
+ @override
77
+ async def atransform(
78
+ self,
79
+ input: AsyncIterator[str | BaseMessage],
80
+ config: RunnableConfig | None = None,
81
+ **kwargs: Any,
82
+ ) -> AsyncIterator[T]:
83
+ """Async transform the input into the output format.
84
+
85
+ Args:
86
+ input: The input to transform.
87
+ config: The configuration to use for the transformation.
88
+ **kwargs: Additional keyword arguments.
89
+
90
+ Yields:
91
+ The transformed output.
92
+ """
93
+ async for chunk in self._atransform_stream_with_config(
94
+ input, self._atransform, config, run_type="parser"
95
+ ):
96
+ yield chunk
97
+
98
+
99
+ class BaseCumulativeTransformOutputParser(BaseTransformOutputParser[T]):
100
+ """Base class for an output parser that can handle streaming input."""
101
+
102
+ diff: bool = False
103
+ """In streaming mode, whether to yield diffs between the previous and current parsed
104
+ output, or just the current parsed output.
105
+ """
106
+
107
+ def _diff(
108
+ self,
109
+ prev: T | None,
110
+ next: T, # noqa: A002
111
+ ) -> T:
112
+ """Convert parsed outputs into a diff format.
113
+
114
+ The semantics of this are up to the output parser.
115
+
116
+ Args:
117
+ prev: The previous parsed output.
118
+ next: The current parsed output.
119
+
120
+ Returns:
121
+ The diff between the previous and current parsed output.
122
+ """
123
+ raise NotImplementedError
124
+
125
+ @override
126
+ def _transform(self, input: Iterator[str | BaseMessage]) -> Iterator[Any]:
127
+ prev_parsed = None
128
+ acc_gen: GenerationChunk | ChatGenerationChunk | None = None
129
+ for chunk in input:
130
+ chunk_gen: GenerationChunk | ChatGenerationChunk
131
+ if isinstance(chunk, BaseMessageChunk):
132
+ chunk_gen = ChatGenerationChunk(message=chunk)
133
+ elif isinstance(chunk, BaseMessage):
134
+ chunk_gen = ChatGenerationChunk(
135
+ message=BaseMessageChunk(**chunk.model_dump())
136
+ )
137
+ else:
138
+ chunk_gen = GenerationChunk(text=chunk)
139
+
140
+ acc_gen = chunk_gen if acc_gen is None else acc_gen + chunk_gen # type: ignore[operator]
141
+
142
+ parsed = self.parse_result([acc_gen], partial=True)
143
+ if parsed is not None and parsed != prev_parsed:
144
+ if self.diff:
145
+ yield self._diff(prev_parsed, parsed)
146
+ else:
147
+ yield parsed
148
+ prev_parsed = parsed
149
+
150
+ @override
151
+ async def _atransform(
152
+ self, input: AsyncIterator[str | BaseMessage]
153
+ ) -> AsyncIterator[T]:
154
+ prev_parsed = None
155
+ acc_gen: GenerationChunk | ChatGenerationChunk | None = None
156
+ async for chunk in input:
157
+ chunk_gen: GenerationChunk | ChatGenerationChunk
158
+ if isinstance(chunk, BaseMessageChunk):
159
+ chunk_gen = ChatGenerationChunk(message=chunk)
160
+ elif isinstance(chunk, BaseMessage):
161
+ chunk_gen = ChatGenerationChunk(
162
+ message=BaseMessageChunk(**chunk.model_dump())
163
+ )
164
+ else:
165
+ chunk_gen = GenerationChunk(text=chunk)
166
+
167
+ acc_gen = chunk_gen if acc_gen is None else acc_gen + chunk_gen # type: ignore[operator]
168
+
169
+ parsed = await self.aparse_result([acc_gen], partial=True)
170
+ if parsed is not None and parsed != prev_parsed:
171
+ if self.diff:
172
+ yield await run_in_executor(None, self._diff, prev_parsed, parsed)
173
+ else:
174
+ yield parsed
175
+ prev_parsed = parsed
@@ -0,0 +1,301 @@
1
+ """Output parser for XML format."""
2
+
3
+ import contextlib
4
+ import re
5
+ import xml
6
+ import xml.etree.ElementTree as ET
7
+ from collections.abc import AsyncIterator, Iterator
8
+ from typing import Any, Literal
9
+ from xml.etree.ElementTree import TreeBuilder
10
+
11
+ from typing_extensions import override
12
+
13
+ from coco_core.exceptions import OutputParserException
14
+ from coco_core.messages import BaseMessage
15
+ from coco_core.output_parsers.transform import BaseTransformOutputParser
16
+ from coco_core.runnables.utils import AddableDict
17
+
18
+ try:
19
+ from defusedxml import ElementTree # type: ignore[import-untyped]
20
+ from defusedxml.ElementTree import XMLParser # type: ignore[import-untyped]
21
+
22
+ _HAS_DEFUSEDXML = True
23
+ except ImportError:
24
+ _HAS_DEFUSEDXML = False
25
+
26
+ XML_FORMAT_INSTRUCTIONS = """The output should be formatted as a XML file.
27
+ 1. Output should conform to the tags below.
28
+ 2. If tags are not given, make them on your own.
29
+ 3. Remember to always open and close all the tags.
30
+
31
+ As an example, for the tags ["foo", "bar", "baz"]:
32
+ 1. String "<foo>\n <bar>\n <baz></baz>\n </bar>\n</foo>" is a well-formatted instance of the schema.
33
+ 2. String "<foo>\n <bar>\n </foo>" is a badly-formatted instance.
34
+ 3. String "<foo>\n <tag>\n </tag>\n</foo>" is a badly-formatted instance.
35
+
36
+ Here are the output tags:
37
+ ```
38
+ {tags}
39
+ ```""" # noqa: E501
40
+
41
+
42
+ class _StreamingParser:
43
+ """Streaming parser for XML.
44
+
45
+ This implementation is pulled into a class to avoid implementation drift between
46
+ `transform` and `atransform` of the `XMLOutputParser`.
47
+ """
48
+
49
+ def __init__(self, parser: Literal["defusedxml", "xml"]) -> None:
50
+ """Initialize the streaming parser.
51
+
52
+ Args:
53
+ parser: Parser to use for XML parsing.
54
+
55
+ Can be either `'defusedxml'` or `'xml'`. See documentation in
56
+ `XMLOutputParser` for more information.
57
+
58
+ Raises:
59
+ ImportError: If `defusedxml` is not installed and the `defusedxml` parser is
60
+ requested.
61
+ """
62
+ if parser == "defusedxml":
63
+ if not _HAS_DEFUSEDXML:
64
+ msg = (
65
+ "defusedxml is not installed. "
66
+ "Please install it to use the defusedxml parser. "
67
+ "You can install it with `pip install defusedxml`. "
68
+ "See https://github.com/tiran/defusedxml for more details"
69
+ )
70
+ raise ImportError(msg)
71
+ parser_ = XMLParser(target=TreeBuilder())
72
+ else:
73
+ parser_ = None
74
+ self.pull_parser = ET.XMLPullParser(["start", "end"], _parser=parser_)
75
+ self.xml_start_re = re.compile(r"<[a-zA-Z:_]")
76
+ self.current_path: list[str] = []
77
+ self.current_path_has_children = False
78
+ self.buffer = ""
79
+ self.xml_started = False
80
+
81
+ def parse(self, chunk: str | BaseMessage) -> Iterator[AddableDict]:
82
+ """Parse a chunk of text.
83
+
84
+ Args:
85
+ chunk: A chunk of text to parse. This can be a `str` or a `BaseMessage`.
86
+
87
+ Yields:
88
+ A `dict` representing the parsed XML element.
89
+
90
+ Raises:
91
+ xml.etree.ElementTree.ParseError: If the XML is not well-formed.
92
+ """
93
+ if isinstance(chunk, BaseMessage):
94
+ # extract text
95
+ chunk_content = chunk.content
96
+ if not isinstance(chunk_content, str):
97
+ # ignore non-string messages (e.g., function calls)
98
+ return
99
+ chunk = chunk_content
100
+ # add chunk to buffer of unprocessed text
101
+ self.buffer += chunk
102
+ # if xml string hasn't started yet, continue to next chunk
103
+ if not self.xml_started:
104
+ if match := self.xml_start_re.search(self.buffer):
105
+ # if xml string has started, remove all text before it
106
+ self.buffer = self.buffer[match.start() :]
107
+ self.xml_started = True
108
+ else:
109
+ return
110
+ # feed buffer to parser
111
+ self.pull_parser.feed(self.buffer)
112
+ self.buffer = ""
113
+ # yield all events
114
+ try:
115
+ events = self.pull_parser.read_events()
116
+ for event, elem in events: # type: ignore[misc]
117
+ if event == "start":
118
+ # update current path
119
+ self.current_path.append(elem.tag) # type: ignore[union-attr]
120
+ self.current_path_has_children = False
121
+ elif event == "end":
122
+ # remove last element from current path
123
+ #
124
+ self.current_path.pop()
125
+ # yield element
126
+ if not self.current_path_has_children:
127
+ yield nested_element(self.current_path, elem) # type: ignore[arg-type]
128
+ # prevent yielding of parent element
129
+ if self.current_path:
130
+ self.current_path_has_children = True
131
+ else:
132
+ self.xml_started = False
133
+ except xml.etree.ElementTree.ParseError:
134
+ # This might be junk at the end of the XML input.
135
+ # Let's check whether the current path is empty.
136
+ if not self.current_path:
137
+ # If it is empty, we can ignore this error.
138
+ return
139
+ else:
140
+ raise
141
+
142
+ def close(self) -> None:
143
+ """Close the parser.
144
+
145
+ This should be called after all chunks have been parsed.
146
+ """
147
+ # Ignore ParseError. This will ignore any incomplete XML at the end of the input
148
+ with contextlib.suppress(xml.etree.ElementTree.ParseError):
149
+ self.pull_parser.close()
150
+
151
+
152
+ class XMLOutputParser(BaseTransformOutputParser[dict[str, Any]]):
153
+ """Parse an output using xml format.
154
+
155
+ Returns a dictionary of tags.
156
+ """
157
+
158
+ tags: list[str] | None = None
159
+ """Tags to tell the LLM to expect in the XML output.
160
+
161
+ Note this may not be perfect depending on the LLM implementation.
162
+
163
+ For example, with `tags=["foo", "bar", "baz"]`:
164
+
165
+ 1. A well-formatted XML instance:
166
+ `'<foo>\n <bar>\n <baz></baz>\n </bar>\n</foo>'`
167
+
168
+ 2. A badly-formatted XML instance (missing closing tag for 'bar'):
169
+ `'<foo>\n <bar>\n </foo>'`
170
+
171
+ 3. A badly-formatted XML instance (unexpected 'tag' element):
172
+ `'<foo>\n <tag>\n </tag>\n</foo>'`
173
+ """
174
+ encoding_matcher: re.Pattern[str] = re.compile(
175
+ r"<([^>]*encoding[^>]*)>\n(.*)", re.MULTILINE | re.DOTALL
176
+ )
177
+
178
+ parser: Literal["defusedxml", "xml"] = "defusedxml"
179
+ """Parser to use for XML parsing.
180
+
181
+ Can be either `'defusedxml'` or `'xml'`.
182
+
183
+ - `'defusedxml'` is the default parser and is used to prevent XML vulnerabilities
184
+ present in some distributions of Python's standard library xml. `defusedxml` is
185
+ a wrapper around the standard library parser that sets up the parser with secure
186
+ defaults.
187
+ - `'xml'` is the standard library parser.
188
+
189
+ !!! warning
190
+
191
+ Use `xml` only if you are sure that your distribution of the standard library is
192
+ not vulnerable to XML vulnerabilities.
193
+
194
+ Review the following resources for more information:
195
+
196
+ * https://docs.python.org/3/library/xml.html#xml-vulnerabilities
197
+ * https://github.com/tiran/defusedxml
198
+
199
+ The standard library relies on [`libexpat`](https://github.com/libexpat/libexpat)
200
+ for parsing XML.
201
+ """
202
+
203
+ def get_format_instructions(self) -> str:
204
+ """Return the format instructions for the XML output."""
205
+ return XML_FORMAT_INSTRUCTIONS.format(tags=self.tags)
206
+
207
+ def parse(self, text: str) -> dict[str, str | list[Any]]:
208
+ """Parse the output of an LLM call.
209
+
210
+ Args:
211
+ text: The output of an LLM call.
212
+
213
+ Returns:
214
+ A `dict` representing the parsed XML.
215
+
216
+ Raises:
217
+ OutputParserException: If the XML is not well-formed.
218
+ ImportError: If `defusedxml` is not installed and the `defusedxml` parser
219
+ is requested.
220
+ """
221
+ # Try to find XML string within triple backticks
222
+ # Imports are temporarily placed here to avoid issue with caching on CI
223
+ # likely if you're reading this you can move them to the top of the file
224
+ if self.parser == "defusedxml":
225
+ if not _HAS_DEFUSEDXML:
226
+ msg = (
227
+ "defusedxml is not installed. "
228
+ "Please install it to use the defusedxml parser. "
229
+ "You can install it with `pip install defusedxml`. "
230
+ "See https://github.com/tiran/defusedxml for more details"
231
+ )
232
+ raise ImportError(msg)
233
+ et = ElementTree # Use the defusedxml parser
234
+ else:
235
+ et = ET # Use the standard library parser
236
+
237
+ match = re.search(r"```(xml)?(.*)```", text, re.DOTALL)
238
+ if match is not None:
239
+ # If match found, use the content within the backticks
240
+ text = match.group(2)
241
+ encoding_match = self.encoding_matcher.search(text)
242
+ if encoding_match:
243
+ text = encoding_match.group(2)
244
+
245
+ text = text.strip()
246
+ try:
247
+ root = et.fromstring(text)
248
+ return self._root_to_dict(root)
249
+ except et.ParseError as e:
250
+ msg = f"Failed to parse XML format from completion {text}. Got: {e}"
251
+ raise OutputParserException(msg, llm_output=text) from e
252
+
253
+ @override
254
+ def _transform(self, input: Iterator[str | BaseMessage]) -> Iterator[AddableDict]:
255
+ streaming_parser = _StreamingParser(self.parser)
256
+ for chunk in input:
257
+ yield from streaming_parser.parse(chunk)
258
+ streaming_parser.close()
259
+
260
+ @override
261
+ async def _atransform(
262
+ self, input: AsyncIterator[str | BaseMessage]
263
+ ) -> AsyncIterator[AddableDict]:
264
+ streaming_parser = _StreamingParser(self.parser)
265
+ async for chunk in input:
266
+ for output in streaming_parser.parse(chunk):
267
+ yield output
268
+ streaming_parser.close()
269
+
270
+ def _root_to_dict(self, root: ET.Element) -> dict[str, str | list[Any]]:
271
+ """Converts xml tree to python dictionary."""
272
+ if root.text and bool(re.search(r"\S", root.text)):
273
+ # If root text contains any non-whitespace character it
274
+ # returns {root.tag: root.text}
275
+ return {root.tag: root.text}
276
+ root_tag: list[Any] = []
277
+ for child in root:
278
+ if len(child) == 0:
279
+ root_tag.append({child.tag: child.text})
280
+ else:
281
+ root_tag.append(self._root_to_dict(child))
282
+ return {root.tag: root_tag}
283
+
284
+ @property
285
+ def _type(self) -> str:
286
+ return "xml"
287
+
288
+
289
+ def nested_element(path: list[str], elem: ET.Element) -> Any:
290
+ """Get nested element from path.
291
+
292
+ Args:
293
+ path: The path to the element.
294
+ elem: The element to extract.
295
+
296
+ Returns:
297
+ The nested element.
298
+ """
299
+ if len(path) == 0:
300
+ return AddableDict({elem.tag: elem.text})
301
+ return AddableDict({path[0]: [nested_element(path[1:], elem)]})
@@ -0,0 +1,66 @@
1
+ """Output classes.
2
+
3
+ Used to represent the output of a language model call and the output of a chat.
4
+
5
+ The top container for information is the `LLMResult` object. `LLMResult` is used by both
6
+ chat models and LLMs. This object contains the output of the language model and any
7
+ additional information that the model provider wants to return.
8
+
9
+ When invoking models via the standard runnable methods (e.g. invoke, batch, etc.):
10
+
11
+ - Chat models will return `AIMessage` objects.
12
+ - LLMs will return regular text strings.
13
+
14
+ In addition, users can access the raw output of either LLMs or chat models via
15
+ callbacks. The `on_chat_model_end` and `on_llm_end` callbacks will return an `LLMResult`
16
+ object containing the generated outputs and any additional information returned by the
17
+ model provider.
18
+
19
+ In general, if information is already available in the AIMessage object, it is
20
+ recommended to access it from there rather than from the `LLMResult` object.
21
+ """
22
+
23
+ from typing import TYPE_CHECKING
24
+
25
+ from coco_core._import_utils import import_attr
26
+
27
+ if TYPE_CHECKING:
28
+ from coco_core.outputs.chat_generation import (
29
+ ChatGeneration,
30
+ ChatGenerationChunk,
31
+ )
32
+ from coco_core.outputs.chat_result import ChatResult
33
+ from coco_core.outputs.generation import Generation, GenerationChunk
34
+ from coco_core.outputs.llm_result import LLMResult
35
+ from coco_core.outputs.run_info import RunInfo
36
+
37
+ __all__ = (
38
+ "ChatGeneration",
39
+ "ChatGenerationChunk",
40
+ "ChatResult",
41
+ "Generation",
42
+ "GenerationChunk",
43
+ "LLMResult",
44
+ "RunInfo",
45
+ )
46
+
47
+ _dynamic_imports = {
48
+ "ChatGeneration": "chat_generation",
49
+ "ChatGenerationChunk": "chat_generation",
50
+ "ChatResult": "chat_result",
51
+ "Generation": "generation",
52
+ "GenerationChunk": "generation",
53
+ "LLMResult": "llm_result",
54
+ "RunInfo": "run_info",
55
+ }
56
+
57
+
58
+ def __getattr__(attr_name: str) -> object:
59
+ module_name = _dynamic_imports.get(attr_name)
60
+ result = import_attr(attr_name, module_name, __spec__.parent)
61
+ globals()[attr_name] = result
62
+ return result
63
+
64
+
65
+ def __dir__() -> list[str]:
66
+ return list(__all__)
@@ -0,0 +1,147 @@
1
+ """Chat generation output classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from pydantic import model_validator
8
+
9
+ from coco_core.messages import BaseMessage, BaseMessageChunk
10
+ from coco_core.outputs.generation import Generation
11
+ from coco_core.utils._merge import merge_dicts
12
+
13
+ if TYPE_CHECKING:
14
+ from typing_extensions import Self
15
+
16
+
17
+ class ChatGeneration(Generation):
18
+ """A single chat generation output.
19
+
20
+ A subclass of `Generation` that represents the response from a chat model that
21
+ generates chat messages.
22
+
23
+ The `message` attribute is a structured representation of the chat message. Most of
24
+ the time, the message will be of type `AIMessage`.
25
+
26
+ Users working with chat models will usually access information via either
27
+ `AIMessage` (returned from runnable interfaces) or `LLMResult` (available via
28
+ callbacks).
29
+ """
30
+
31
+ text: str = ""
32
+ """The text contents of the output message.
33
+
34
+ !!! warning "SHOULD NOT BE SET DIRECTLY!"
35
+
36
+ """
37
+ message: BaseMessage
38
+ """The message output by the chat model."""
39
+
40
+ # Override type to be ChatGeneration, ignore mypy error as this is intentional
41
+ type: Literal["ChatGeneration"] = "ChatGeneration" # type: ignore[assignment]
42
+ """Type is used exclusively for serialization purposes."""
43
+
44
+ @model_validator(mode="after")
45
+ def set_text(self) -> Self:
46
+ """Set the text attribute to be the contents of the message."""
47
+ # Check for legacy blocks with "text" key but no "type" field.
48
+ # Otherwise, delegate to `message.text`.
49
+ if isinstance(self.message.content, list):
50
+ has_legacy_blocks = any(
51
+ isinstance(block, dict)
52
+ and "text" in block
53
+ and block.get("type") is None
54
+ for block in self.message.content
55
+ )
56
+
57
+ if has_legacy_blocks:
58
+ blocks = []
59
+ for block in self.message.content:
60
+ if isinstance(block, str):
61
+ blocks.append(block)
62
+ elif isinstance(block, dict):
63
+ block_type = block.get("type")
64
+ if block_type == "text" or (
65
+ block_type is None and "text" in block
66
+ ):
67
+ blocks.append(block.get("text", ""))
68
+ self.text = "".join(blocks)
69
+ else:
70
+ self.text = self.message.text
71
+ else:
72
+ self.text = self.message.text
73
+
74
+ return self
75
+
76
+
77
+ class ChatGenerationChunk(ChatGeneration):
78
+ """`ChatGeneration` chunk.
79
+
80
+ `ChatGeneration` chunks can be concatenated with other `ChatGeneration` chunks.
81
+ """
82
+
83
+ message: BaseMessageChunk
84
+ """The message chunk output by the chat model."""
85
+ # Override type to be ChatGeneration, ignore mypy error as this is intentional
86
+
87
+ type: Literal["ChatGenerationChunk"] = "ChatGenerationChunk" # type: ignore[assignment]
88
+ """Type is used exclusively for serialization purposes."""
89
+
90
+ def __add__(
91
+ self, other: ChatGenerationChunk | list[ChatGenerationChunk]
92
+ ) -> ChatGenerationChunk:
93
+ """Concatenate two `ChatGenerationChunk`s.
94
+
95
+ Args:
96
+ other: The other `ChatGenerationChunk` or list of `ChatGenerationChunk` to
97
+ concatenate.
98
+
99
+ Raises:
100
+ TypeError: If other is not a `ChatGenerationChunk` or list of
101
+ `ChatGenerationChunk`.
102
+
103
+ Returns:
104
+ A new `ChatGenerationChunk` concatenated from self and other.
105
+ """
106
+ if isinstance(other, ChatGenerationChunk):
107
+ generation_info = merge_dicts(
108
+ self.generation_info or {},
109
+ other.generation_info or {},
110
+ )
111
+ return ChatGenerationChunk(
112
+ message=self.message + other.message,
113
+ generation_info=generation_info or None,
114
+ )
115
+ if isinstance(other, list) and all(
116
+ isinstance(x, ChatGenerationChunk) for x in other
117
+ ):
118
+ generation_info = merge_dicts(
119
+ self.generation_info or {},
120
+ *[chunk.generation_info for chunk in other if chunk.generation_info],
121
+ )
122
+ return ChatGenerationChunk(
123
+ message=self.message + [chunk.message for chunk in other],
124
+ generation_info=generation_info or None,
125
+ )
126
+ msg = f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'"
127
+ raise TypeError(msg)
128
+
129
+
130
+ def merge_chat_generation_chunks(
131
+ chunks: list[ChatGenerationChunk],
132
+ ) -> ChatGenerationChunk | None:
133
+ """Merge a list of `ChatGenerationChunk`s into a single `ChatGenerationChunk`.
134
+
135
+ Args:
136
+ chunks: A list of `ChatGenerationChunk` to merge.
137
+
138
+ Returns:
139
+ A merged `ChatGenerationChunk`, or `None` if the input list is empty.
140
+ """
141
+ if not chunks:
142
+ return None
143
+
144
+ if len(chunks) == 1:
145
+ return chunks[0]
146
+
147
+ return chunks[0] + chunks[1:]