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,631 @@
1
+ """`Runnable` that manages chat message history for another `Runnable`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable, Sequence
7
+ from types import GenericAlias
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ )
12
+
13
+ from pydantic import BaseModel
14
+ from typing_extensions import override
15
+
16
+ from coco_core._api.deprecation import warn_deprecated
17
+ from coco_core.chat_history import BaseChatMessageHistory
18
+ from coco_core.load.load import load
19
+ from coco_core.messages import AIMessage, BaseMessage, HumanMessage
20
+ from coco_core.runnables.base import Runnable, RunnableBindingBase, RunnableLambda
21
+ from coco_core.runnables.passthrough import RunnablePassthrough
22
+ from coco_core.runnables.utils import (
23
+ ConfigurableFieldSpec,
24
+ Output,
25
+ get_unique_config_specs,
26
+ )
27
+ from coco_core.utils.pydantic import create_model_v2
28
+
29
+ if TYPE_CHECKING:
30
+ from coco_core.language_models.base import LanguageModelLike
31
+ from coco_core.runnables.config import RunnableConfig
32
+ from coco_core.tracers.schemas import Run
33
+
34
+
35
+ MessagesOrDictWithMessages = Sequence["BaseMessage"] | dict[str, Any]
36
+ GetSessionHistoryCallable = Callable[..., BaseChatMessageHistory]
37
+
38
+
39
+ class RunnableWithMessageHistory(RunnableBindingBase[Any, Any]): # type: ignore[no-redef]
40
+ """`Runnable` that manages chat message history for another `Runnable`.
41
+
42
+ A chat message history is a sequence of messages that represent a conversation.
43
+
44
+ `RunnableWithMessageHistory` wraps another `Runnable` and manages the chat message
45
+ history for it; it is responsible for reading and updating the chat message
46
+ history.
47
+
48
+ The formats supported for the inputs and outputs of the wrapped `Runnable`
49
+ are described below.
50
+
51
+ `RunnableWithMessageHistory` must always be called with a config that contains
52
+ the appropriate parameters for the chat message history factory.
53
+
54
+ By default, the `Runnable` is expected to take a single configuration parameter
55
+ called `session_id` which is a string. This parameter is used to create a new
56
+ or look up an existing chat message history that matches the given `session_id`.
57
+
58
+ In this case, the invocation would look like this:
59
+
60
+ `with_history.invoke(..., config={"configurable": {"session_id": "bar"}})`
61
+ ; e.g., `{"configurable": {"session_id": "<SESSION_ID>"}}`.
62
+
63
+ The configuration can be customized by passing in a list of
64
+ `ConfigurableFieldSpec` objects to the `history_factory_config` parameter (see
65
+ example below).
66
+
67
+ In the examples, we will use a chat message history with an in-memory
68
+ implementation to make it easy to experiment and see the results.
69
+
70
+ For production use cases, you will want to use a persistent implementation
71
+ of chat message history, such as `RedisChatMessageHistory`.
72
+
73
+ Example: Chat message history with an in-memory implementation for testing.
74
+
75
+ ```python
76
+ from operator import itemgetter
77
+
78
+ from coco_openai.chat_models import ChatOpenAI
79
+
80
+ from coco_core.chat_history import BaseChatMessageHistory
81
+ from coco_core.documents import Document
82
+ from coco_core.messages import BaseMessage, AIMessage
83
+ from coco_core.prompts import ChatPromptTemplate, MessagesPlaceholder
84
+ from pydantic import BaseModel, Field
85
+ from coco_core.runnables import (
86
+ RunnableLambda,
87
+ ConfigurableFieldSpec,
88
+ RunnablePassthrough,
89
+ )
90
+ from coco_core.runnables.history import RunnableWithMessageHistory
91
+
92
+
93
+ class InMemoryHistory(BaseChatMessageHistory, BaseModel):
94
+ \"\"\"In memory implementation of chat message history.\"\"\"
95
+
96
+ messages: list[BaseMessage] = Field(default_factory=list)
97
+
98
+ def add_messages(self, messages: list[BaseMessage]) -> None:
99
+ \"\"\"Add a list of messages to the store\"\"\"
100
+ self.messages.extend(messages)
101
+
102
+ def clear(self) -> None:
103
+ self.messages = []
104
+
105
+ # Here we use a global variable to store the chat message history.
106
+ # This will make it easier to inspect it to see the underlying results.
107
+ store = {}
108
+
109
+ def get_by_session_id(session_id: str) -> BaseChatMessageHistory:
110
+ if session_id not in store:
111
+ store[session_id] = InMemoryHistory()
112
+ return store[session_id]
113
+
114
+
115
+ history = get_by_session_id("1")
116
+ history.add_message(AIMessage(content="hello"))
117
+ print(store) # noqa: T201
118
+
119
+ ```
120
+
121
+ Example where the wrapped `Runnable` takes a dictionary input:
122
+
123
+ ```python
124
+ from typing import Optional
125
+
126
+ from coco_anthropic import ChatAnthropic
127
+ from coco_core.prompts import ChatPromptTemplate, MessagesPlaceholder
128
+ from coco_core.runnables.history import RunnableWithMessageHistory
129
+
130
+
131
+ prompt = ChatPromptTemplate.from_messages(
132
+ [
133
+ ("system", "You're an assistant who's good at {ability}"),
134
+ MessagesPlaceholder(variable_name="history"),
135
+ ("human", "{question}"),
136
+ ]
137
+ )
138
+
139
+ chain = prompt | ChatAnthropic(model="claude-sonnet-5")
140
+
141
+ chain_with_history = RunnableWithMessageHistory(
142
+ chain,
143
+ # Uses the get_by_session_id function defined in the example
144
+ # above.
145
+ get_by_session_id,
146
+ input_messages_key="question",
147
+ history_messages_key="history",
148
+ )
149
+
150
+ print(
151
+ chain_with_history.invoke( # noqa: T201
152
+ {"ability": "math", "question": "What does cosine mean?"},
153
+ config={"configurable": {"session_id": "foo"}},
154
+ )
155
+ )
156
+
157
+ # Uses the store defined in the example above.
158
+ print(store) # noqa: T201
159
+
160
+ print(
161
+ chain_with_history.invoke( # noqa: T201
162
+ {"ability": "math", "question": "What's its inverse"},
163
+ config={"configurable": {"session_id": "foo"}},
164
+ )
165
+ )
166
+
167
+ print(store) # noqa: T201
168
+ ```
169
+
170
+ Example where the session factory takes two keys (`user_id` and `conversation_id`):
171
+
172
+ ```python
173
+ store = {}
174
+
175
+
176
+ def get_session_history(
177
+ user_id: str, conversation_id: str
178
+ ) -> BaseChatMessageHistory:
179
+ if (user_id, conversation_id) not in store:
180
+ store[(user_id, conversation_id)] = InMemoryHistory()
181
+ return store[(user_id, conversation_id)]
182
+
183
+
184
+ prompt = ChatPromptTemplate.from_messages(
185
+ [
186
+ ("system", "You're an assistant who's good at {ability}"),
187
+ MessagesPlaceholder(variable_name="history"),
188
+ ("human", "{question}"),
189
+ ]
190
+ )
191
+
192
+ chain = prompt | ChatAnthropic(model="claude-sonnet-5")
193
+
194
+ with_message_history = RunnableWithMessageHistory(
195
+ chain,
196
+ get_session_history=get_session_history,
197
+ input_messages_key="question",
198
+ history_messages_key="history",
199
+ history_factory_config=[
200
+ ConfigurableFieldSpec(
201
+ id="user_id",
202
+ annotation=str,
203
+ name="User ID",
204
+ description="Unique identifier for the user.",
205
+ default="",
206
+ is_shared=True,
207
+ ),
208
+ ConfigurableFieldSpec(
209
+ id="conversation_id",
210
+ annotation=str,
211
+ name="Conversation ID",
212
+ description="Unique identifier for the conversation.",
213
+ default="",
214
+ is_shared=True,
215
+ ),
216
+ ],
217
+ )
218
+
219
+ with_message_history.invoke(
220
+ {"ability": "math", "question": "What does cosine mean?"},
221
+ config={"configurable": {"user_id": "123", "conversation_id": "1"}},
222
+ )
223
+ ```
224
+ """
225
+
226
+ get_session_history: GetSessionHistoryCallable
227
+ """Function that returns a new `BaseChatMessageHistory`.
228
+
229
+ This function should either take a single positional argument `session_id` of type
230
+ string and return a corresponding chat message history instance
231
+ """
232
+ input_messages_key: str | None = None
233
+ """Must be specified if the base `Runnable` accepts a `dict` as input.
234
+ The key in the input `dict` that contains the messages.
235
+ """
236
+ output_messages_key: str | None = None
237
+ """Must be specified if the base `Runnable` returns a `dict` as output.
238
+ The key in the output `dict` that contains the messages.
239
+ """
240
+ history_messages_key: str | None = None
241
+ """Must be specified if the base `Runnable` accepts a `dict` as input and expects a
242
+ separate key for historical messages.
243
+ """
244
+ history_factory_config: Sequence[ConfigurableFieldSpec]
245
+ """Configure fields that should be passed to the chat history factory.
246
+
247
+ See `ConfigurableFieldSpec` for more details.
248
+ """
249
+
250
+ def __init__(
251
+ self,
252
+ runnable: Runnable[
253
+ list[BaseMessage], str | BaseMessage | MessagesOrDictWithMessages
254
+ ]
255
+ | Runnable[dict[str, Any], str | BaseMessage | MessagesOrDictWithMessages]
256
+ | LanguageModelLike,
257
+ get_session_history: GetSessionHistoryCallable,
258
+ *,
259
+ input_messages_key: str | None = None,
260
+ output_messages_key: str | None = None,
261
+ history_messages_key: str | None = None,
262
+ history_factory_config: Sequence[ConfigurableFieldSpec] | None = None,
263
+ **kwargs: Any,
264
+ ) -> None:
265
+ """Initialize `RunnableWithMessageHistory`.
266
+
267
+ Args:
268
+ runnable: The base `Runnable` to be wrapped.
269
+
270
+ Must take as input one of:
271
+
272
+ 1. A list of `BaseMessage`
273
+ 2. A `dict` with one key for all messages
274
+ 3. A `dict` with one key for the current input string/message(s) and
275
+ a separate key for historical messages. If the input key points
276
+ to a string, it will be treated as a `HumanMessage` in history.
277
+
278
+ Must return as output one of:
279
+
280
+ 1. A string which can be treated as an `AIMessage`
281
+ 2. A `BaseMessage` or sequence of `BaseMessage`
282
+ 3. A `dict` with a key for a `BaseMessage` or sequence of
283
+ `BaseMessage`
284
+
285
+ get_session_history: Function that returns a new `BaseChatMessageHistory`.
286
+
287
+ This function should either take a single positional argument
288
+ `session_id` of type string and return a corresponding
289
+ chat message history instance.
290
+
291
+ ```python
292
+ def get_session_history(
293
+ session_id: str, *, user_id: str | None = None
294
+ ) -> BaseChatMessageHistory: ...
295
+ ```
296
+
297
+ Or it should take keyword arguments that match the keys of
298
+ `session_history_config_specs` and return a corresponding
299
+ chat message history instance.
300
+
301
+ ```python
302
+ def get_session_history(
303
+ *,
304
+ user_id: str,
305
+ thread_id: str,
306
+ ) -> BaseChatMessageHistory: ...
307
+ ```
308
+
309
+ input_messages_key: Must be specified if the base runnable accepts a `dict`
310
+ as input.
311
+ output_messages_key: Must be specified if the base runnable returns a `dict`
312
+ as output.
313
+ history_messages_key: Must be specified if the base runnable accepts a
314
+ `dict` as input and expects a separate key for historical messages.
315
+ history_factory_config: Configure fields that should be passed to the
316
+ chat history factory. See `ConfigurableFieldSpec` for more details.
317
+
318
+ Specifying these allows you to pass multiple config keys into the
319
+ `get_session_history` factory.
320
+ **kwargs: Arbitrary additional kwargs to pass to parent class
321
+ `RunnableBindingBase` init.
322
+
323
+ """
324
+ warn_deprecated(
325
+ since="1.3.3",
326
+ message=(
327
+ "RunnableWithMessageHistory is deprecated. "
328
+ "Use CocoGraph's built-in persistence instead."
329
+ ),
330
+ removal="2.0.0",
331
+ )
332
+ history_chain: Runnable[Any, Any] = RunnableLambda(
333
+ self._enter_history, self._aenter_history
334
+ ).with_config(run_name="load_history")
335
+ messages_key = history_messages_key or input_messages_key
336
+ if messages_key:
337
+ history_chain = RunnablePassthrough.assign(
338
+ **{messages_key: history_chain}
339
+ ).with_config(run_name="insert_history")
340
+
341
+ runnable_sync = runnable.with_listeners(on_end=self._exit_history)
342
+ runnable_async = runnable.with_alisteners(on_end=self._aexit_history)
343
+
344
+ def _call_runnable_sync(_input: Any) -> Runnable[Any, Any]:
345
+ return runnable_sync
346
+
347
+ async def _call_runnable_async(_input: Any) -> Runnable[Any, Any]:
348
+ return runnable_async
349
+
350
+ bound = (
351
+ history_chain
352
+ | RunnableLambda(
353
+ _call_runnable_sync,
354
+ _call_runnable_async,
355
+ ).with_config(run_name="check_sync_or_async")
356
+ ).with_config(run_name="RunnableWithMessageHistory")
357
+
358
+ if history_factory_config:
359
+ config_specs = history_factory_config
360
+ else:
361
+ # If not provided, then we'll use the default session_id field
362
+ config_specs = [
363
+ ConfigurableFieldSpec(
364
+ id="session_id",
365
+ annotation=str,
366
+ name="Session ID",
367
+ description="Unique identifier for a session.",
368
+ default="",
369
+ is_shared=True,
370
+ ),
371
+ ]
372
+
373
+ super().__init__(
374
+ get_session_history=get_session_history,
375
+ input_messages_key=input_messages_key,
376
+ output_messages_key=output_messages_key,
377
+ bound=bound,
378
+ history_messages_key=history_messages_key,
379
+ history_factory_config=config_specs,
380
+ **kwargs,
381
+ )
382
+ self._history_chain = history_chain
383
+
384
+ @property
385
+ @override
386
+ def config_specs(self) -> list[ConfigurableFieldSpec]:
387
+ """Get the configuration specs for the `RunnableWithMessageHistory`."""
388
+ return get_unique_config_specs(
389
+ super().config_specs + list(self.history_factory_config)
390
+ )
391
+
392
+ @override
393
+ def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]:
394
+ fields: dict[str, Any] = {}
395
+ if self.input_messages_key and self.history_messages_key:
396
+ fields[self.input_messages_key] = (
397
+ str | BaseMessage | Sequence[BaseMessage],
398
+ ...,
399
+ )
400
+ elif self.input_messages_key:
401
+ fields[self.input_messages_key] = (Sequence[BaseMessage], ...)
402
+ else:
403
+ return create_model_v2(
404
+ "RunnableWithChatHistoryInput",
405
+ module_name=self.__class__.__module__,
406
+ root=(Sequence[BaseMessage], ...),
407
+ )
408
+ return create_model_v2(
409
+ "RunnableWithChatHistoryInput",
410
+ field_definitions=fields,
411
+ module_name=self.__class__.__module__,
412
+ )
413
+
414
+ @property
415
+ @override
416
+ def OutputType(self) -> type[Output]:
417
+ return self._history_chain.OutputType
418
+
419
+ @override
420
+ def get_output_schema(
421
+ self, config: RunnableConfig | None = None
422
+ ) -> type[BaseModel]:
423
+ """Get a Pydantic model that can be used to validate output to the `Runnable`.
424
+
425
+ `Runnable` objects that leverage the `configurable_fields` and
426
+ `configurable_alternatives` methods will have a dynamic output schema that
427
+ depends on which configuration the `Runnable` is invoked with.
428
+
429
+ This method allows to get an output schema for a specific configuration.
430
+
431
+ Args:
432
+ config: A config to use when generating the schema.
433
+
434
+ Returns:
435
+ A Pydantic model that can be used to validate output.
436
+ """
437
+ root_type = self.OutputType
438
+
439
+ if (
440
+ inspect.isclass(root_type)
441
+ and not isinstance(root_type, GenericAlias)
442
+ and issubclass(root_type, BaseModel)
443
+ ):
444
+ return root_type
445
+
446
+ return create_model_v2(
447
+ "RunnableWithChatHistoryOutput",
448
+ root=root_type,
449
+ module_name=self.__class__.__module__,
450
+ )
451
+
452
+ def _get_input_messages(
453
+ self, input_val: str | BaseMessage | Sequence[BaseMessage] | dict[str, Any]
454
+ ) -> list[BaseMessage]:
455
+ # If dictionary, try to pluck the single key representing messages
456
+ if isinstance(input_val, dict):
457
+ if self.input_messages_key:
458
+ key = self.input_messages_key
459
+ elif len(input_val) == 1:
460
+ key = next(iter(input_val.keys()))
461
+ else:
462
+ key = "input"
463
+ input_val = input_val[key]
464
+
465
+ # If value is a string, convert to a human message
466
+ if isinstance(input_val, str):
467
+ return [HumanMessage(content=input_val)]
468
+ # If value is a single message, convert to a list
469
+ if isinstance(input_val, BaseMessage):
470
+ return [input_val]
471
+ # If value is a list or tuple...
472
+ if isinstance(input_val, (list, tuple)):
473
+ # Handle empty case
474
+ if len(input_val) == 0:
475
+ return list(input_val)
476
+ # If is a list of list, then return the first value
477
+ # This occurs for chat models - since we batch inputs
478
+ if isinstance(input_val[0], list):
479
+ if len(input_val) != 1:
480
+ msg = f"Expected a single list of messages. Got {input_val}."
481
+ raise ValueError(msg)
482
+ return input_val[0]
483
+ return list(input_val)
484
+ msg = (
485
+ f"Expected str, BaseMessage, list[BaseMessage], or tuple[BaseMessage]. "
486
+ f"Got {input_val}."
487
+ )
488
+ raise ValueError(msg)
489
+
490
+ def _get_output_messages(
491
+ self, output_val: str | BaseMessage | Sequence[BaseMessage] | dict[str, Any]
492
+ ) -> list[BaseMessage]:
493
+ # If dictionary, try to pluck the single key representing messages
494
+ if isinstance(output_val, dict):
495
+ if self.output_messages_key:
496
+ key = self.output_messages_key
497
+ elif len(output_val) == 1:
498
+ key = next(iter(output_val.keys()))
499
+ else:
500
+ key = "output"
501
+ # If you are wrapping a chat model directly
502
+ # The output is actually this weird generations object
503
+ if key not in output_val and "generations" in output_val:
504
+ output_val = output_val["generations"][0][0]["message"]
505
+ else:
506
+ output_val = output_val[key]
507
+
508
+ if isinstance(output_val, str):
509
+ return [AIMessage(content=output_val)]
510
+ # If value is a single message, convert to a list
511
+ if isinstance(output_val, BaseMessage):
512
+ return [output_val]
513
+ if isinstance(output_val, (list, tuple)):
514
+ return list(output_val)
515
+ msg = (
516
+ f"Expected str, BaseMessage, list[BaseMessage], or tuple[BaseMessage]. "
517
+ f"Got {output_val}."
518
+ )
519
+ raise ValueError(msg)
520
+
521
+ def _enter_history(self, value: Any, config: RunnableConfig) -> list[BaseMessage]:
522
+ hist: BaseChatMessageHistory = config["configurable"]["message_history"]
523
+ messages = hist.messages.copy()
524
+
525
+ if not self.history_messages_key:
526
+ # return all messages
527
+ input_val = (
528
+ value if not self.input_messages_key else value[self.input_messages_key]
529
+ )
530
+ messages += self._get_input_messages(input_val)
531
+ return messages
532
+
533
+ async def _aenter_history(
534
+ self, value: dict[str, Any], config: RunnableConfig
535
+ ) -> list[BaseMessage]:
536
+ hist: BaseChatMessageHistory = config["configurable"]["message_history"]
537
+ messages = (await hist.aget_messages()).copy()
538
+
539
+ if not self.history_messages_key:
540
+ # return all messages
541
+ input_val = (
542
+ value if not self.input_messages_key else value[self.input_messages_key]
543
+ )
544
+ messages += self._get_input_messages(input_val)
545
+ return messages
546
+
547
+ def _exit_history(self, run: Run, config: RunnableConfig) -> None:
548
+ hist: BaseChatMessageHistory = config["configurable"]["message_history"]
549
+
550
+ # Get the input messages
551
+ inputs = load(run.inputs, allowed_objects="messages")
552
+ input_messages = self._get_input_messages(inputs)
553
+ # If historic messages were prepended to the input messages, remove them to
554
+ # avoid adding duplicate messages to history.
555
+ if not self.history_messages_key:
556
+ historic_messages = config["configurable"]["message_history"].messages
557
+ input_messages = input_messages[len(historic_messages) :]
558
+
559
+ # Get the output messages
560
+ output_val = load(run.outputs, allowed_objects="messages")
561
+ output_messages = self._get_output_messages(output_val)
562
+ hist.add_messages(input_messages + output_messages)
563
+
564
+ async def _aexit_history(self, run: Run, config: RunnableConfig) -> None:
565
+ hist: BaseChatMessageHistory = config["configurable"]["message_history"]
566
+
567
+ # Get the input messages
568
+ inputs = load(run.inputs, allowed_objects="messages")
569
+ input_messages = self._get_input_messages(inputs)
570
+ # If historic messages were prepended to the input messages, remove them to
571
+ # avoid adding duplicate messages to history.
572
+ if not self.history_messages_key:
573
+ historic_messages = await hist.aget_messages()
574
+ input_messages = input_messages[len(historic_messages) :]
575
+
576
+ # Get the output messages
577
+ output_val = load(run.outputs, allowed_objects="messages")
578
+ output_messages = self._get_output_messages(output_val)
579
+ await hist.aadd_messages(input_messages + output_messages)
580
+
581
+ def _merge_configs(self, *configs: RunnableConfig | None) -> RunnableConfig:
582
+ config = super()._merge_configs(*configs)
583
+ expected_keys = [field_spec.id for field_spec in self.history_factory_config]
584
+
585
+ configurable = config.get("configurable", {})
586
+
587
+ missing_keys = set(expected_keys) - set(configurable.keys())
588
+ parameter_names = _get_parameter_names(self.get_session_history)
589
+
590
+ if missing_keys and parameter_names:
591
+ example_input = {self.input_messages_key: "foo"}
592
+ example_configurable = dict.fromkeys(missing_keys, "[your-value-here]")
593
+ example_config = {"configurable": example_configurable}
594
+ msg = (
595
+ f"Missing keys {sorted(missing_keys)} in config['configurable'] "
596
+ f"Expected keys are {sorted(expected_keys)}."
597
+ f"When using via .invoke() or .stream(), pass in a config; "
598
+ f"e.g., chain.invoke({example_input}, {example_config})"
599
+ )
600
+ raise ValueError(msg)
601
+
602
+ if len(expected_keys) == 1:
603
+ if parameter_names:
604
+ # If arity = 1, then invoke function by positional arguments
605
+ message_history = self.get_session_history(
606
+ configurable[expected_keys[0]]
607
+ )
608
+ else:
609
+ if not config:
610
+ config["configurable"] = {}
611
+ message_history = self.get_session_history()
612
+ else:
613
+ # otherwise verify that names of keys patch and invoke by named arguments
614
+ if set(expected_keys) != set(parameter_names):
615
+ msg = (
616
+ f"Expected keys {sorted(expected_keys)} do not match parameter "
617
+ f"names {sorted(parameter_names)} of get_session_history."
618
+ )
619
+ raise ValueError(msg)
620
+
621
+ message_history = self.get_session_history(
622
+ **{key: configurable[key] for key in expected_keys}
623
+ )
624
+ config["configurable"]["message_history"] = message_history
625
+ return config
626
+
627
+
628
+ def _get_parameter_names(callable_: GetSessionHistoryCallable) -> list[str]:
629
+ """Get the parameter names of the `Callable`."""
630
+ sig = inspect.signature(callable_)
631
+ return list(sig.parameters.keys())