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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import warnings
|
|
3
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
4
|
+
from typing import (
|
|
5
|
+
Annotated,
|
|
6
|
+
Any,
|
|
7
|
+
Literal,
|
|
8
|
+
TypeVar,
|
|
9
|
+
cast,
|
|
10
|
+
get_type_hints,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from coco_core.language_models import (
|
|
14
|
+
BaseChatModel,
|
|
15
|
+
LanguageModelInput,
|
|
16
|
+
LanguageModelLike,
|
|
17
|
+
)
|
|
18
|
+
from coco_core.messages import (
|
|
19
|
+
AIMessage,
|
|
20
|
+
AnyMessage,
|
|
21
|
+
BaseMessage,
|
|
22
|
+
SystemMessage,
|
|
23
|
+
ToolMessage,
|
|
24
|
+
)
|
|
25
|
+
from coco_core.runnables import (
|
|
26
|
+
Runnable,
|
|
27
|
+
RunnableBinding,
|
|
28
|
+
RunnableConfig,
|
|
29
|
+
RunnableSequence,
|
|
30
|
+
)
|
|
31
|
+
from coco_core.tools import BaseTool
|
|
32
|
+
from coco_graph._internal._runnable import RunnableCallable, RunnableLike
|
|
33
|
+
from coco_graph._internal._typing import MISSING
|
|
34
|
+
from coco_graph.errors import ErrorCode, create_error_message
|
|
35
|
+
from coco_graph.graph import END, StateGraph
|
|
36
|
+
from coco_graph.graph.message import add_messages
|
|
37
|
+
from coco_graph.graph.state import CompiledStateGraph
|
|
38
|
+
from coco_graph.managed import RemainingSteps
|
|
39
|
+
from coco_graph.runtime import Runtime
|
|
40
|
+
from coco_graph.store.base import BaseStore
|
|
41
|
+
from coco_graph.types import Checkpointer, Send
|
|
42
|
+
from coco_graph.typing import ContextT
|
|
43
|
+
from coco_graph.warnings import CocoGraphDeprecatedSinceV10
|
|
44
|
+
from pydantic import BaseModel
|
|
45
|
+
from typing_extensions import NotRequired, TypedDict, deprecated
|
|
46
|
+
|
|
47
|
+
from coco_graph.prebuilt.tool_node import ToolCallWithContext, ToolNode
|
|
48
|
+
|
|
49
|
+
StructuredResponse = dict | BaseModel
|
|
50
|
+
StructuredResponseSchema = dict | type[BaseModel]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@deprecated(
|
|
54
|
+
"AgentState has been moved to `coco.agents`. Please update your import to `from coco.agents import AgentState`.",
|
|
55
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
56
|
+
)
|
|
57
|
+
class AgentState(TypedDict):
|
|
58
|
+
"""The state of the agent."""
|
|
59
|
+
|
|
60
|
+
messages: Annotated[Sequence[BaseMessage], add_messages]
|
|
61
|
+
|
|
62
|
+
remaining_steps: NotRequired[RemainingSteps]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@deprecated(
|
|
66
|
+
"AgentStatePydantic has been deprecated in favor of AgentState in `coco.agents`.",
|
|
67
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
68
|
+
)
|
|
69
|
+
class AgentStatePydantic(BaseModel):
|
|
70
|
+
"""The state of the agent."""
|
|
71
|
+
|
|
72
|
+
messages: Annotated[Sequence[BaseMessage], add_messages]
|
|
73
|
+
|
|
74
|
+
remaining_steps: RemainingSteps = 25
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
with warnings.catch_warnings():
|
|
78
|
+
warnings.filterwarnings(
|
|
79
|
+
"ignore",
|
|
80
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
81
|
+
message="AgentState has been moved to `coco.agents`.*",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@deprecated(
|
|
85
|
+
"AgentStateWithStructuredResponse has been deprecated in favor of AgentState in `coco.agents`.",
|
|
86
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
87
|
+
)
|
|
88
|
+
class AgentStateWithStructuredResponse(AgentState):
|
|
89
|
+
"""The state of the agent with a structured response."""
|
|
90
|
+
|
|
91
|
+
structured_response: StructuredResponse
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
with warnings.catch_warnings():
|
|
95
|
+
warnings.filterwarnings(
|
|
96
|
+
"ignore",
|
|
97
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
98
|
+
message="AgentStatePydantic has been deprecated in favor of AgentState in `coco.agents`.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@deprecated(
|
|
102
|
+
"AgentStateWithStructuredResponsePydantic has been deprecated in favor of AgentState in `coco.agents`.",
|
|
103
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
104
|
+
)
|
|
105
|
+
class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):
|
|
106
|
+
"""The state of the agent with a structured response."""
|
|
107
|
+
|
|
108
|
+
structured_response: StructuredResponse
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
with warnings.catch_warnings():
|
|
112
|
+
warnings.filterwarnings(
|
|
113
|
+
"ignore",
|
|
114
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
115
|
+
)
|
|
116
|
+
StateSchema = TypeVar("StateSchema", bound=AgentState | AgentStatePydantic)
|
|
117
|
+
StateSchemaType = type[StateSchema]
|
|
118
|
+
|
|
119
|
+
PROMPT_RUNNABLE_NAME = "Prompt"
|
|
120
|
+
|
|
121
|
+
Prompt = (
|
|
122
|
+
SystemMessage
|
|
123
|
+
| str
|
|
124
|
+
| Callable[[StateSchema], LanguageModelInput]
|
|
125
|
+
| Runnable[StateSchema, LanguageModelInput]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_state_value(state: StateSchema, key: str, default: Any = None) -> Any:
|
|
130
|
+
return (
|
|
131
|
+
state.get(key, default)
|
|
132
|
+
if isinstance(state, dict)
|
|
133
|
+
else getattr(state, key, default)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_prompt_runnable(prompt: Prompt | None) -> Runnable:
|
|
138
|
+
prompt_runnable: Runnable
|
|
139
|
+
if prompt is None:
|
|
140
|
+
prompt_runnable = RunnableCallable(
|
|
141
|
+
lambda state: _get_state_value(state, "messages"), name=PROMPT_RUNNABLE_NAME
|
|
142
|
+
)
|
|
143
|
+
elif isinstance(prompt, str):
|
|
144
|
+
_system_message: BaseMessage = SystemMessage(content=prompt)
|
|
145
|
+
prompt_runnable = RunnableCallable(
|
|
146
|
+
lambda state: [_system_message] + _get_state_value(state, "messages"),
|
|
147
|
+
name=PROMPT_RUNNABLE_NAME,
|
|
148
|
+
)
|
|
149
|
+
elif isinstance(prompt, SystemMessage):
|
|
150
|
+
prompt_runnable = RunnableCallable(
|
|
151
|
+
lambda state: [prompt] + _get_state_value(state, "messages"),
|
|
152
|
+
name=PROMPT_RUNNABLE_NAME,
|
|
153
|
+
)
|
|
154
|
+
elif inspect.iscoroutinefunction(prompt):
|
|
155
|
+
prompt_runnable = RunnableCallable(
|
|
156
|
+
None,
|
|
157
|
+
prompt,
|
|
158
|
+
name=PROMPT_RUNNABLE_NAME,
|
|
159
|
+
)
|
|
160
|
+
elif callable(prompt):
|
|
161
|
+
prompt_runnable = RunnableCallable(
|
|
162
|
+
prompt,
|
|
163
|
+
name=PROMPT_RUNNABLE_NAME,
|
|
164
|
+
)
|
|
165
|
+
elif isinstance(prompt, Runnable):
|
|
166
|
+
prompt_runnable = prompt
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError(f"Got unexpected type for `prompt`: {type(prompt)}")
|
|
169
|
+
|
|
170
|
+
return prompt_runnable
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _should_bind_tools(
|
|
174
|
+
model: LanguageModelLike, tools: Sequence[BaseTool], num_builtin: int = 0
|
|
175
|
+
) -> bool:
|
|
176
|
+
if isinstance(model, RunnableSequence):
|
|
177
|
+
model = next(
|
|
178
|
+
(
|
|
179
|
+
step
|
|
180
|
+
for step in model.steps
|
|
181
|
+
if isinstance(step, (RunnableBinding, BaseChatModel))
|
|
182
|
+
),
|
|
183
|
+
model,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if not isinstance(model, RunnableBinding):
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
if "tools" not in model.kwargs:
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
bound_tools = model.kwargs["tools"]
|
|
193
|
+
if len(tools) != len(bound_tools) - num_builtin:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
"Number of tools in the model.bind_tools() and tools passed to create_react_agent must match"
|
|
196
|
+
f" Got {len(tools)} tools, expected {len(bound_tools) - num_builtin}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
tool_names = set(tool.name for tool in tools)
|
|
200
|
+
bound_tool_names = set()
|
|
201
|
+
for bound_tool in bound_tools:
|
|
202
|
+
# OpenAI-style tool
|
|
203
|
+
if bound_tool.get("type") == "function":
|
|
204
|
+
bound_tool_name = bound_tool["function"]["name"]
|
|
205
|
+
# Anthropic-style tool
|
|
206
|
+
elif bound_tool.get("name"):
|
|
207
|
+
bound_tool_name = bound_tool["name"]
|
|
208
|
+
else:
|
|
209
|
+
# unknown tool type so we'll ignore it
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
bound_tool_names.add(bound_tool_name)
|
|
213
|
+
|
|
214
|
+
if missing_tools := tool_names - bound_tool_names:
|
|
215
|
+
raise ValueError(f"Missing tools '{missing_tools}' in the model.bind_tools()")
|
|
216
|
+
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_model(model: LanguageModelLike) -> BaseChatModel:
|
|
221
|
+
"""Get the underlying model from a RunnableBinding or return the model itself."""
|
|
222
|
+
if isinstance(model, RunnableSequence):
|
|
223
|
+
model = next(
|
|
224
|
+
(
|
|
225
|
+
step
|
|
226
|
+
for step in model.steps
|
|
227
|
+
if isinstance(step, (RunnableBinding, BaseChatModel))
|
|
228
|
+
),
|
|
229
|
+
model,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if isinstance(model, RunnableBinding):
|
|
233
|
+
model = model.bound
|
|
234
|
+
|
|
235
|
+
if not isinstance(model, BaseChatModel):
|
|
236
|
+
raise TypeError(
|
|
237
|
+
f"Expected `model` to be a ChatModel or RunnableBinding (e.g. model.bind_tools(...)), got {type(model)}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return model
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _validate_chat_history(
|
|
244
|
+
messages: Sequence[BaseMessage],
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Validate that all tool calls in AIMessages have a corresponding ToolMessage."""
|
|
247
|
+
all_tool_calls = [
|
|
248
|
+
tool_call
|
|
249
|
+
for message in messages
|
|
250
|
+
if isinstance(message, AIMessage)
|
|
251
|
+
for tool_call in message.tool_calls
|
|
252
|
+
]
|
|
253
|
+
tool_call_ids_with_results = {
|
|
254
|
+
message.tool_call_id for message in messages if isinstance(message, ToolMessage)
|
|
255
|
+
}
|
|
256
|
+
tool_calls_without_results = [
|
|
257
|
+
tool_call
|
|
258
|
+
for tool_call in all_tool_calls
|
|
259
|
+
if tool_call["id"] not in tool_call_ids_with_results
|
|
260
|
+
]
|
|
261
|
+
if not tool_calls_without_results:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
error_message = create_error_message(
|
|
265
|
+
message="Found AIMessages with tool_calls that do not have a corresponding ToolMessage. "
|
|
266
|
+
f"Here are the first few of those tool calls: {tool_calls_without_results[:3]}.\n\n"
|
|
267
|
+
"Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage "
|
|
268
|
+
"(result of a tool invocation to return to the LLM) - this is required by most LLM providers.",
|
|
269
|
+
error_code=ErrorCode.INVALID_CHAT_HISTORY,
|
|
270
|
+
)
|
|
271
|
+
raise ValueError(error_message)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@deprecated(
|
|
275
|
+
"create_react_agent has been moved to `coco.agents`. Please update your import to `from coco.agents import create_agent`.",
|
|
276
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
277
|
+
)
|
|
278
|
+
def create_react_agent(
|
|
279
|
+
model: str
|
|
280
|
+
| LanguageModelLike
|
|
281
|
+
| Callable[[StateSchema, Runtime[ContextT]], BaseChatModel]
|
|
282
|
+
| Callable[[StateSchema, Runtime[ContextT]], Awaitable[BaseChatModel]]
|
|
283
|
+
| Callable[
|
|
284
|
+
[StateSchema, Runtime[ContextT]], Runnable[LanguageModelInput, BaseMessage]
|
|
285
|
+
]
|
|
286
|
+
| Callable[
|
|
287
|
+
[StateSchema, Runtime[ContextT]],
|
|
288
|
+
Awaitable[Runnable[LanguageModelInput, BaseMessage]],
|
|
289
|
+
],
|
|
290
|
+
tools: Sequence[BaseTool | Callable | dict[str, Any]] | ToolNode,
|
|
291
|
+
*,
|
|
292
|
+
prompt: Prompt | None = None,
|
|
293
|
+
response_format: StructuredResponseSchema
|
|
294
|
+
| tuple[str, StructuredResponseSchema]
|
|
295
|
+
| None = None,
|
|
296
|
+
pre_model_hook: RunnableLike | None = None,
|
|
297
|
+
post_model_hook: RunnableLike | None = None,
|
|
298
|
+
state_schema: StateSchemaType | None = None,
|
|
299
|
+
context_schema: type[Any] | None = None,
|
|
300
|
+
checkpointer: Checkpointer | None = None,
|
|
301
|
+
store: BaseStore | None = None,
|
|
302
|
+
interrupt_before: list[str] | None = None,
|
|
303
|
+
interrupt_after: list[str] | None = None,
|
|
304
|
+
debug: bool = False,
|
|
305
|
+
version: Literal["v1", "v2"] = "v2",
|
|
306
|
+
name: str | None = None,
|
|
307
|
+
**deprecated_kwargs: Any,
|
|
308
|
+
) -> CompiledStateGraph:
|
|
309
|
+
"""Creates an agent graph that calls tools in a loop until a stopping condition is met.
|
|
310
|
+
|
|
311
|
+
!!! warning
|
|
312
|
+
|
|
313
|
+
This function is deprecated in favor of
|
|
314
|
+
[`create_agent`][coco.agents.create_agent] from the `coco`
|
|
315
|
+
package, which provides an equivalent agent factory with a flexible
|
|
316
|
+
middleware system. For migration guidance, see
|
|
317
|
+
[Migrating from CocoGraph v0](https://docs.langchain.com/oss/python/migrate/langgraph-v1).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
model: The language model for the agent. Supports static and dynamic
|
|
321
|
+
model selection.
|
|
322
|
+
|
|
323
|
+
- **Static model**: A chat model instance (e.g.,
|
|
324
|
+
[`ChatOpenAI`][coco_openai.ChatOpenAI]) or string identifier (e.g.,
|
|
325
|
+
`"openai:gpt-4"`)
|
|
326
|
+
- **Dynamic model**: A callable with signature
|
|
327
|
+
`(state, runtime) -> BaseChatModel` that returns different models
|
|
328
|
+
based on runtime context
|
|
329
|
+
|
|
330
|
+
If the model has tools bound via `bind_tools` or other configurations,
|
|
331
|
+
the return type should be a `Runnable[LanguageModelInput, BaseMessage]`
|
|
332
|
+
Coroutines are also supported, allowing for asynchronous model selection.
|
|
333
|
+
|
|
334
|
+
Dynamic functions receive graph state and runtime, enabling
|
|
335
|
+
context-dependent model selection. Must return a `BaseChatModel`
|
|
336
|
+
instance. For tool calling, bind tools using `.bind_tools()`.
|
|
337
|
+
Bound tools must be a subset of the `tools` parameter.
|
|
338
|
+
|
|
339
|
+
!!! example "Dynamic model"
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from dataclasses import dataclass
|
|
343
|
+
|
|
344
|
+
@dataclass
|
|
345
|
+
class ModelContext:
|
|
346
|
+
model_name: str = "gpt-3.5-turbo"
|
|
347
|
+
|
|
348
|
+
# Instantiate models globally
|
|
349
|
+
gpt4_model = ChatOpenAI(model="gpt-4")
|
|
350
|
+
gpt35_model = ChatOpenAI(model="gpt-3.5-turbo")
|
|
351
|
+
|
|
352
|
+
def select_model(state: AgentState, runtime: Runtime[ModelContext]) -> ChatOpenAI:
|
|
353
|
+
model_name = runtime.context.model_name
|
|
354
|
+
model = gpt4_model if model_name == "gpt-4" else gpt35_model
|
|
355
|
+
return model.bind_tools(tools)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
!!! note "Dynamic Model Requirements"
|
|
359
|
+
|
|
360
|
+
Ensure returned models have appropriate tools bound via
|
|
361
|
+
`.bind_tools()` and support required functionality. Bound tools
|
|
362
|
+
must be a subset of those specified in the `tools` parameter.
|
|
363
|
+
|
|
364
|
+
tools: A list of tools or a `ToolNode` instance.
|
|
365
|
+
If an empty list is provided, the agent will consist of a single LLM node without tool calling.
|
|
366
|
+
prompt: An optional prompt for the LLM. Can take a few different forms:
|
|
367
|
+
|
|
368
|
+
- `str`: This is converted to a `SystemMessage` and added to the beginning of the list of messages in `state["messages"]`.
|
|
369
|
+
- `SystemMessage`: this is added to the beginning of the list of messages in `state["messages"]`.
|
|
370
|
+
- `Callable`: This function should take in full graph state and the output is then passed to the language model.
|
|
371
|
+
- `Runnable`: This runnable should take in full graph state and the output is then passed to the language model.
|
|
372
|
+
|
|
373
|
+
response_format: An optional schema for the final agent output.
|
|
374
|
+
|
|
375
|
+
If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.
|
|
376
|
+
|
|
377
|
+
If not provided, `structured_response` will not be present in the output state.
|
|
378
|
+
|
|
379
|
+
Can be passed in as:
|
|
380
|
+
|
|
381
|
+
- An OpenAI function/tool schema,
|
|
382
|
+
- A JSON Schema,
|
|
383
|
+
- A TypedDict class,
|
|
384
|
+
- A Pydantic class.
|
|
385
|
+
- A tuple `(prompt, schema)`, where schema is one of the above.
|
|
386
|
+
The prompt will be used together with the model that is being used to
|
|
387
|
+
generate the structured response.
|
|
388
|
+
|
|
389
|
+
!!! Important
|
|
390
|
+
`response_format` requires the model to support `.with_structured_output`
|
|
391
|
+
|
|
392
|
+
!!! Note
|
|
393
|
+
The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.
|
|
394
|
+
This is not the only strategy to get structured responses, see more options in [this guide](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/).
|
|
395
|
+
|
|
396
|
+
pre_model_hook: An optional node to add before the `agent` node (i.e., the node that calls the LLM).
|
|
397
|
+
Useful for managing long message histories (e.g., message trimming, summarization, etc.).
|
|
398
|
+
Pre-model hook must be a callable or a runnable that takes in current graph state and returns a state update in the form of
|
|
399
|
+
```python
|
|
400
|
+
# At least one of `messages` or `llm_input_messages` MUST be provided
|
|
401
|
+
{
|
|
402
|
+
# If provided, will UPDATE the `messages` in the state
|
|
403
|
+
"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), ...],
|
|
404
|
+
# If provided, will be used as the input to the LLM,
|
|
405
|
+
# and will NOT UPDATE `messages` in the state
|
|
406
|
+
"llm_input_messages": [...],
|
|
407
|
+
# Any other state keys that need to be propagated
|
|
408
|
+
...
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
!!! Important
|
|
413
|
+
At least one of `messages` or `llm_input_messages` MUST be provided and will be used as an input to the `agent` node.
|
|
414
|
+
The rest of the keys will be added to the graph state.
|
|
415
|
+
|
|
416
|
+
!!! Warning
|
|
417
|
+
If you are returning `messages` in the pre-model hook, you should OVERWRITE the `messages` key by doing the following:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
{
|
|
421
|
+
"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *new_messages]
|
|
422
|
+
...
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
post_model_hook: An optional node to add after the `agent` node (i.e., the node that calls the LLM).
|
|
426
|
+
Useful for implementing human-in-the-loop, guardrails, validation, or other post-processing.
|
|
427
|
+
Post-model hook must be a callable or a runnable that takes in current graph state and returns a state update.
|
|
428
|
+
|
|
429
|
+
!!! Note
|
|
430
|
+
Only available with `version="v2"`.
|
|
431
|
+
state_schema: An optional state schema that defines graph state.
|
|
432
|
+
Must have `messages` and `remaining_steps` keys.
|
|
433
|
+
Defaults to `AgentState` that defines those two keys.
|
|
434
|
+
!!! Note
|
|
435
|
+
`remaining_steps` is used to limit the number of steps the react agent can take.
|
|
436
|
+
Calculated roughly as `recursion_limit` - `total_steps_taken`.
|
|
437
|
+
If `remaining_steps` is less than 2 and tool calls are present in the response,
|
|
438
|
+
the react agent will return a final AI Message with
|
|
439
|
+
the content "Sorry, need more steps to process this request.".
|
|
440
|
+
No `GraphRecusionError` will be raised in this case.
|
|
441
|
+
|
|
442
|
+
context_schema: An optional schema for runtime context.
|
|
443
|
+
checkpointer: An optional checkpoint saver object. This is used for persisting
|
|
444
|
+
the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation).
|
|
445
|
+
store: An optional store object. This is used for persisting data
|
|
446
|
+
across multiple threads (e.g., multiple conversations / users).
|
|
447
|
+
interrupt_before: An optional list of node names to interrupt before.
|
|
448
|
+
Should be one of the following: `"agent"`, `"tools"`.
|
|
449
|
+
|
|
450
|
+
This is useful if you want to add a user confirmation or other interrupt before taking an action.
|
|
451
|
+
interrupt_after: An optional list of node names to interrupt after.
|
|
452
|
+
Should be one of the following: `"agent"`, `"tools"`.
|
|
453
|
+
|
|
454
|
+
This is useful if you want to return directly or run additional processing on an output.
|
|
455
|
+
debug: A flag indicating whether to enable debug mode.
|
|
456
|
+
version: Determines the version of the graph to create.
|
|
457
|
+
|
|
458
|
+
Can be one of:
|
|
459
|
+
|
|
460
|
+
- `"v1"`: The tool node processes a single message. All tool
|
|
461
|
+
calls in the message are executed in parallel within the tool node.
|
|
462
|
+
- `"v2"`: The tool node processes a tool call.
|
|
463
|
+
Tool calls are distributed across multiple instances of the tool
|
|
464
|
+
node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send)
|
|
465
|
+
API.
|
|
466
|
+
name: An optional name for the `CompiledStateGraph`.
|
|
467
|
+
This name will be automatically used when adding ReAct agent graph to another graph as a subgraph node -
|
|
468
|
+
particularly useful for building multi-agent systems.
|
|
469
|
+
|
|
470
|
+
!!! warning "`config_schema` Deprecated"
|
|
471
|
+
The `config_schema` parameter is deprecated in v0.6.0 and support will be removed in v2.0.0.
|
|
472
|
+
Please use `context_schema` instead to specify the schema for run-scoped context.
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
A compiled Coco `Runnable` that can be used for chat interactions.
|
|
477
|
+
|
|
478
|
+
The "agent" node calls the language model with the messages list (after applying the prompt).
|
|
479
|
+
If the resulting AIMessage contains `tool_calls`, the graph will then call the ["tools"][coco_graph.prebuilt.tool_node.ToolNode].
|
|
480
|
+
The "tools" node executes the tools (1 tool per `tool_call`) and adds the responses to the messages list
|
|
481
|
+
as `ToolMessage` objects. The agent node then calls the language model again.
|
|
482
|
+
The process repeats until no more `tool_calls` are present in the response.
|
|
483
|
+
The agent then returns the full list of messages as a dictionary containing the key `'messages'`.
|
|
484
|
+
|
|
485
|
+
``` mermaid
|
|
486
|
+
sequenceDiagram
|
|
487
|
+
participant U as User
|
|
488
|
+
participant A as LLM
|
|
489
|
+
participant T as Tools
|
|
490
|
+
U->>A: Initial input
|
|
491
|
+
Note over A: Prompt + LLM
|
|
492
|
+
loop while tool_calls present
|
|
493
|
+
A->>T: Execute tools
|
|
494
|
+
T-->>A: ToolMessage for each tool_calls
|
|
495
|
+
end
|
|
496
|
+
A->>U: Return final state
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Example:
|
|
500
|
+
```python
|
|
501
|
+
from coco_graph.prebuilt import create_react_agent
|
|
502
|
+
|
|
503
|
+
def check_weather(location: str) -> str:
|
|
504
|
+
'''Return the weather forecast for the specified location.'''
|
|
505
|
+
return f"It's always sunny in {location}"
|
|
506
|
+
|
|
507
|
+
graph = create_react_agent(
|
|
508
|
+
"anthropic:claude-3-7-sonnet-latest",
|
|
509
|
+
tools=[check_weather],
|
|
510
|
+
prompt="You are a helpful assistant",
|
|
511
|
+
)
|
|
512
|
+
inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
|
513
|
+
for chunk in graph.stream(inputs, stream_mode="updates"):
|
|
514
|
+
print(chunk)
|
|
515
|
+
```
|
|
516
|
+
"""
|
|
517
|
+
if (
|
|
518
|
+
config_schema := deprecated_kwargs.pop("config_schema", MISSING)
|
|
519
|
+
) is not MISSING:
|
|
520
|
+
warnings.warn(
|
|
521
|
+
"`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",
|
|
522
|
+
category=CocoGraphDeprecatedSinceV10,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if context_schema is None:
|
|
526
|
+
context_schema = config_schema
|
|
527
|
+
|
|
528
|
+
if len(deprecated_kwargs) > 0:
|
|
529
|
+
raise TypeError(
|
|
530
|
+
f"create_react_agent() got unexpected keyword arguments: {deprecated_kwargs}"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if version not in ("v1", "v2"):
|
|
534
|
+
raise ValueError(
|
|
535
|
+
f"Invalid version {version}. Supported versions are 'v1' and 'v2'."
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if state_schema is not None:
|
|
539
|
+
required_keys = {"messages", "remaining_steps"}
|
|
540
|
+
if response_format is not None:
|
|
541
|
+
required_keys.add("structured_response")
|
|
542
|
+
|
|
543
|
+
schema_keys = set(get_type_hints(state_schema))
|
|
544
|
+
if missing_keys := required_keys - set(schema_keys):
|
|
545
|
+
raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
|
|
546
|
+
|
|
547
|
+
if state_schema is None:
|
|
548
|
+
state_schema = (
|
|
549
|
+
AgentStateWithStructuredResponse
|
|
550
|
+
if response_format is not None
|
|
551
|
+
else AgentState
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
llm_builtin_tools: list[dict] = []
|
|
555
|
+
if isinstance(tools, ToolNode):
|
|
556
|
+
tool_classes = list(tools.tools_by_name.values())
|
|
557
|
+
tool_node = tools
|
|
558
|
+
else:
|
|
559
|
+
llm_builtin_tools = [t for t in tools if isinstance(t, dict)]
|
|
560
|
+
tool_node = ToolNode([t for t in tools if not isinstance(t, dict)])
|
|
561
|
+
tool_classes = list(tool_node.tools_by_name.values())
|
|
562
|
+
|
|
563
|
+
is_dynamic_model = not isinstance(model, (str, Runnable)) and callable(model)
|
|
564
|
+
is_async_dynamic_model = is_dynamic_model and inspect.iscoroutinefunction(model)
|
|
565
|
+
|
|
566
|
+
tool_calling_enabled = len(tool_classes) > 0
|
|
567
|
+
|
|
568
|
+
if not is_dynamic_model:
|
|
569
|
+
if isinstance(model, str):
|
|
570
|
+
try:
|
|
571
|
+
from coco.chat_models import ( # type: ignore[import-not-found]
|
|
572
|
+
init_chat_model,
|
|
573
|
+
)
|
|
574
|
+
except ImportError:
|
|
575
|
+
raise ImportError(
|
|
576
|
+
"Please install coco (`pip install coco`) to "
|
|
577
|
+
"use '<provider>:<model>' string syntax for `model` parameter."
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
model = cast(BaseChatModel, init_chat_model(model))
|
|
581
|
+
|
|
582
|
+
if (
|
|
583
|
+
_should_bind_tools(model, tool_classes, num_builtin=len(llm_builtin_tools)) # type: ignore[arg-type]
|
|
584
|
+
and len(tool_classes + llm_builtin_tools) > 0
|
|
585
|
+
):
|
|
586
|
+
model = cast(BaseChatModel, model).bind_tools(
|
|
587
|
+
tool_classes + llm_builtin_tools # type: ignore[operator]
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
static_model: Runnable | None = _get_prompt_runnable(prompt) | model # type: ignore[operator]
|
|
591
|
+
else:
|
|
592
|
+
# For dynamic models, we'll create the runnable at runtime
|
|
593
|
+
static_model = None
|
|
594
|
+
|
|
595
|
+
# If any of the tools are configured to return_directly after running,
|
|
596
|
+
# our graph needs to check if these were called
|
|
597
|
+
should_return_direct = {t.name for t in tool_classes if t.return_direct}
|
|
598
|
+
|
|
599
|
+
def _resolve_model(
|
|
600
|
+
state: StateSchema, runtime: Runtime[ContextT]
|
|
601
|
+
) -> LanguageModelLike:
|
|
602
|
+
"""Resolve the model to use, handling both static and dynamic models."""
|
|
603
|
+
if is_dynamic_model:
|
|
604
|
+
return _get_prompt_runnable(prompt) | model(state, runtime) # type: ignore[operator]
|
|
605
|
+
else:
|
|
606
|
+
return static_model
|
|
607
|
+
|
|
608
|
+
async def _aresolve_model(
|
|
609
|
+
state: StateSchema, runtime: Runtime[ContextT]
|
|
610
|
+
) -> LanguageModelLike:
|
|
611
|
+
"""Async resolve the model to use, handling both static and dynamic models."""
|
|
612
|
+
if is_async_dynamic_model:
|
|
613
|
+
resolved_model = await model(state, runtime) # type: ignore[misc,operator]
|
|
614
|
+
return _get_prompt_runnable(prompt) | resolved_model
|
|
615
|
+
elif is_dynamic_model:
|
|
616
|
+
return _get_prompt_runnable(prompt) | model(state, runtime) # type: ignore[operator]
|
|
617
|
+
else:
|
|
618
|
+
return static_model
|
|
619
|
+
|
|
620
|
+
def _are_more_steps_needed(state: StateSchema, response: BaseMessage) -> bool:
|
|
621
|
+
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
|
|
622
|
+
all_tools_return_direct = (
|
|
623
|
+
all(call["name"] in should_return_direct for call in response.tool_calls)
|
|
624
|
+
if isinstance(response, AIMessage)
|
|
625
|
+
else False
|
|
626
|
+
)
|
|
627
|
+
remaining_steps = _get_state_value(state, "remaining_steps", None)
|
|
628
|
+
if remaining_steps is not None:
|
|
629
|
+
if remaining_steps < 1 and all_tools_return_direct:
|
|
630
|
+
return True
|
|
631
|
+
elif remaining_steps < 2 and has_tool_calls:
|
|
632
|
+
return True
|
|
633
|
+
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
def _get_model_input_state(state: StateSchema) -> StateSchema:
|
|
637
|
+
if pre_model_hook is not None:
|
|
638
|
+
messages = (
|
|
639
|
+
_get_state_value(state, "llm_input_messages")
|
|
640
|
+
) or _get_state_value(state, "messages")
|
|
641
|
+
error_msg = f"Expected input to call_model to have 'llm_input_messages' or 'messages' key, but got {state}"
|
|
642
|
+
else:
|
|
643
|
+
messages = _get_state_value(state, "messages")
|
|
644
|
+
error_msg = (
|
|
645
|
+
f"Expected input to call_model to have 'messages' key, but got {state}"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
if messages is None:
|
|
649
|
+
raise ValueError(error_msg)
|
|
650
|
+
|
|
651
|
+
_validate_chat_history(messages)
|
|
652
|
+
# we're passing messages under `messages` key, as this is expected by the prompt
|
|
653
|
+
if isinstance(state_schema, type) and issubclass(state_schema, BaseModel):
|
|
654
|
+
state.messages = messages # type: ignore
|
|
655
|
+
else:
|
|
656
|
+
state["messages"] = messages # type: ignore
|
|
657
|
+
|
|
658
|
+
return state
|
|
659
|
+
|
|
660
|
+
# Define the function that calls the model
|
|
661
|
+
def call_model(
|
|
662
|
+
state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig
|
|
663
|
+
) -> StateSchema:
|
|
664
|
+
if is_async_dynamic_model:
|
|
665
|
+
msg = (
|
|
666
|
+
"Async model callable provided but agent invoked synchronously. "
|
|
667
|
+
"Use agent.ainvoke() or agent.astream(), or "
|
|
668
|
+
"provide a sync model callable."
|
|
669
|
+
)
|
|
670
|
+
raise RuntimeError(msg)
|
|
671
|
+
|
|
672
|
+
model_input = _get_model_input_state(state)
|
|
673
|
+
|
|
674
|
+
if is_dynamic_model:
|
|
675
|
+
# Resolve dynamic model at runtime and apply prompt
|
|
676
|
+
dynamic_model = _resolve_model(state, runtime)
|
|
677
|
+
response = cast(AIMessage, dynamic_model.invoke(model_input, config)) # type: ignore[arg-type]
|
|
678
|
+
else:
|
|
679
|
+
response = cast(AIMessage, static_model.invoke(model_input, config)) # type: ignore[union-attr]
|
|
680
|
+
|
|
681
|
+
# add agent name to the AIMessage
|
|
682
|
+
response.name = name
|
|
683
|
+
|
|
684
|
+
if _are_more_steps_needed(state, response):
|
|
685
|
+
return {
|
|
686
|
+
"messages": [
|
|
687
|
+
AIMessage(
|
|
688
|
+
id=response.id,
|
|
689
|
+
content="Sorry, need more steps to process this request.",
|
|
690
|
+
)
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
# We return a list, because this will get added to the existing list
|
|
694
|
+
return {"messages": [response]}
|
|
695
|
+
|
|
696
|
+
async def acall_model(
|
|
697
|
+
state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig
|
|
698
|
+
) -> StateSchema:
|
|
699
|
+
model_input = _get_model_input_state(state)
|
|
700
|
+
|
|
701
|
+
if is_dynamic_model:
|
|
702
|
+
# Resolve dynamic model at runtime and apply prompt
|
|
703
|
+
# (supports both sync and async)
|
|
704
|
+
dynamic_model = await _aresolve_model(state, runtime)
|
|
705
|
+
response = cast(AIMessage, await dynamic_model.ainvoke(model_input, config)) # type: ignore[arg-type]
|
|
706
|
+
else:
|
|
707
|
+
response = cast(AIMessage, await static_model.ainvoke(model_input, config)) # type: ignore[union-attr]
|
|
708
|
+
|
|
709
|
+
# add agent name to the AIMessage
|
|
710
|
+
response.name = name
|
|
711
|
+
if _are_more_steps_needed(state, response):
|
|
712
|
+
return {
|
|
713
|
+
"messages": [
|
|
714
|
+
AIMessage(
|
|
715
|
+
id=response.id,
|
|
716
|
+
content="Sorry, need more steps to process this request.",
|
|
717
|
+
)
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
# We return a list, because this will get added to the existing list
|
|
721
|
+
return {"messages": [response]}
|
|
722
|
+
|
|
723
|
+
input_schema: StateSchemaType
|
|
724
|
+
if pre_model_hook is not None:
|
|
725
|
+
# Dynamically create a schema that inherits from state_schema and adds 'llm_input_messages'
|
|
726
|
+
if isinstance(state_schema, type) and issubclass(state_schema, BaseModel):
|
|
727
|
+
# For Pydantic schemas
|
|
728
|
+
from pydantic import create_model
|
|
729
|
+
|
|
730
|
+
input_schema = create_model(
|
|
731
|
+
"CallModelInputSchema",
|
|
732
|
+
llm_input_messages=(list[AnyMessage], ...),
|
|
733
|
+
__base__=state_schema,
|
|
734
|
+
)
|
|
735
|
+
else:
|
|
736
|
+
# For TypedDict schemas
|
|
737
|
+
class CallModelInputSchema(state_schema): # type: ignore
|
|
738
|
+
llm_input_messages: list[AnyMessage]
|
|
739
|
+
|
|
740
|
+
input_schema = CallModelInputSchema
|
|
741
|
+
else:
|
|
742
|
+
input_schema = state_schema
|
|
743
|
+
|
|
744
|
+
def generate_structured_response(
|
|
745
|
+
state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig
|
|
746
|
+
) -> StateSchema:
|
|
747
|
+
if is_async_dynamic_model:
|
|
748
|
+
msg = (
|
|
749
|
+
"Async model callable provided but agent invoked synchronously. "
|
|
750
|
+
"Use agent.ainvoke() or agent.astream(), or provide a sync model callable."
|
|
751
|
+
)
|
|
752
|
+
raise RuntimeError(msg)
|
|
753
|
+
|
|
754
|
+
messages = _get_state_value(state, "messages")
|
|
755
|
+
structured_response_schema = response_format
|
|
756
|
+
if isinstance(response_format, tuple):
|
|
757
|
+
system_prompt, structured_response_schema = response_format
|
|
758
|
+
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
|
759
|
+
|
|
760
|
+
resolved_model = _resolve_model(state, runtime)
|
|
761
|
+
model_with_structured_output = _get_model(
|
|
762
|
+
resolved_model
|
|
763
|
+
).with_structured_output(
|
|
764
|
+
cast(StructuredResponseSchema, structured_response_schema)
|
|
765
|
+
)
|
|
766
|
+
response = model_with_structured_output.invoke(messages, config)
|
|
767
|
+
return {"structured_response": response}
|
|
768
|
+
|
|
769
|
+
async def agenerate_structured_response(
|
|
770
|
+
state: StateSchema, runtime: Runtime[ContextT], config: RunnableConfig
|
|
771
|
+
) -> StateSchema:
|
|
772
|
+
messages = _get_state_value(state, "messages")
|
|
773
|
+
structured_response_schema = response_format
|
|
774
|
+
if isinstance(response_format, tuple):
|
|
775
|
+
system_prompt, structured_response_schema = response_format
|
|
776
|
+
messages = [SystemMessage(content=system_prompt)] + list(messages)
|
|
777
|
+
|
|
778
|
+
resolved_model = await _aresolve_model(state, runtime)
|
|
779
|
+
model_with_structured_output = _get_model(
|
|
780
|
+
resolved_model
|
|
781
|
+
).with_structured_output(
|
|
782
|
+
cast(StructuredResponseSchema, structured_response_schema)
|
|
783
|
+
)
|
|
784
|
+
response = await model_with_structured_output.ainvoke(messages, config)
|
|
785
|
+
return {"structured_response": response}
|
|
786
|
+
|
|
787
|
+
if not tool_calling_enabled:
|
|
788
|
+
# Define a new graph
|
|
789
|
+
workflow = StateGraph(state_schema=state_schema, context_schema=context_schema)
|
|
790
|
+
workflow.add_node(
|
|
791
|
+
"agent",
|
|
792
|
+
RunnableCallable(call_model, acall_model),
|
|
793
|
+
input_schema=input_schema,
|
|
794
|
+
)
|
|
795
|
+
if pre_model_hook is not None:
|
|
796
|
+
workflow.add_node("pre_model_hook", pre_model_hook) # type: ignore[arg-type]
|
|
797
|
+
workflow.add_edge("pre_model_hook", "agent")
|
|
798
|
+
entrypoint = "pre_model_hook"
|
|
799
|
+
else:
|
|
800
|
+
entrypoint = "agent"
|
|
801
|
+
|
|
802
|
+
workflow.set_entry_point(entrypoint)
|
|
803
|
+
|
|
804
|
+
if post_model_hook is not None:
|
|
805
|
+
workflow.add_node("post_model_hook", post_model_hook) # type: ignore[arg-type]
|
|
806
|
+
workflow.add_edge("agent", "post_model_hook")
|
|
807
|
+
|
|
808
|
+
if response_format is not None:
|
|
809
|
+
workflow.add_node(
|
|
810
|
+
"generate_structured_response",
|
|
811
|
+
RunnableCallable(
|
|
812
|
+
generate_structured_response,
|
|
813
|
+
agenerate_structured_response,
|
|
814
|
+
),
|
|
815
|
+
)
|
|
816
|
+
if post_model_hook is not None:
|
|
817
|
+
workflow.add_edge("post_model_hook", "generate_structured_response")
|
|
818
|
+
else:
|
|
819
|
+
workflow.add_edge("agent", "generate_structured_response")
|
|
820
|
+
|
|
821
|
+
return workflow.compile(
|
|
822
|
+
checkpointer=checkpointer,
|
|
823
|
+
store=store,
|
|
824
|
+
interrupt_before=interrupt_before,
|
|
825
|
+
interrupt_after=interrupt_after,
|
|
826
|
+
debug=debug,
|
|
827
|
+
name=name,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Define the function that determines whether to continue or not
|
|
831
|
+
def should_continue(state: StateSchema) -> str | list[Send]:
|
|
832
|
+
messages = _get_state_value(state, "messages")
|
|
833
|
+
last_message = messages[-1]
|
|
834
|
+
# If there is no function call, then we finish
|
|
835
|
+
if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
|
|
836
|
+
if post_model_hook is not None:
|
|
837
|
+
return "post_model_hook"
|
|
838
|
+
elif response_format is not None:
|
|
839
|
+
return "generate_structured_response"
|
|
840
|
+
else:
|
|
841
|
+
return END
|
|
842
|
+
# Otherwise if there is, we continue
|
|
843
|
+
else:
|
|
844
|
+
if version == "v1":
|
|
845
|
+
return "tools"
|
|
846
|
+
elif version == "v2":
|
|
847
|
+
if post_model_hook is not None:
|
|
848
|
+
return "post_model_hook"
|
|
849
|
+
return [
|
|
850
|
+
Send(
|
|
851
|
+
"tools",
|
|
852
|
+
ToolCallWithContext(
|
|
853
|
+
__type="tool_call_with_context",
|
|
854
|
+
tool_call=call,
|
|
855
|
+
state=state,
|
|
856
|
+
),
|
|
857
|
+
)
|
|
858
|
+
for call in last_message.tool_calls
|
|
859
|
+
]
|
|
860
|
+
|
|
861
|
+
# Define a new graph
|
|
862
|
+
workflow = StateGraph(
|
|
863
|
+
state_schema=state_schema or AgentState, context_schema=context_schema
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
# Define the two nodes we will cycle between
|
|
867
|
+
workflow.add_node(
|
|
868
|
+
"agent",
|
|
869
|
+
RunnableCallable(call_model, acall_model),
|
|
870
|
+
input_schema=input_schema,
|
|
871
|
+
)
|
|
872
|
+
workflow.add_node("tools", tool_node)
|
|
873
|
+
|
|
874
|
+
# Optionally add a pre-model hook node that will be called
|
|
875
|
+
# every time before the "agent" (LLM-calling node)
|
|
876
|
+
if pre_model_hook is not None:
|
|
877
|
+
workflow.add_node("pre_model_hook", pre_model_hook) # type: ignore[arg-type]
|
|
878
|
+
workflow.add_edge("pre_model_hook", "agent")
|
|
879
|
+
entrypoint = "pre_model_hook"
|
|
880
|
+
else:
|
|
881
|
+
entrypoint = "agent"
|
|
882
|
+
|
|
883
|
+
# Set the entrypoint as `agent`
|
|
884
|
+
# This means that this node is the first one called
|
|
885
|
+
workflow.set_entry_point(entrypoint)
|
|
886
|
+
|
|
887
|
+
agent_paths = []
|
|
888
|
+
post_model_hook_paths = [entrypoint, "tools"]
|
|
889
|
+
|
|
890
|
+
# Add a post model hook node if post_model_hook is provided
|
|
891
|
+
if post_model_hook is not None:
|
|
892
|
+
workflow.add_node("post_model_hook", post_model_hook) # type: ignore[arg-type]
|
|
893
|
+
agent_paths.append("post_model_hook")
|
|
894
|
+
workflow.add_edge("agent", "post_model_hook")
|
|
895
|
+
else:
|
|
896
|
+
agent_paths.append("tools")
|
|
897
|
+
|
|
898
|
+
# Add a structured output node if response_format is provided
|
|
899
|
+
if response_format is not None:
|
|
900
|
+
workflow.add_node(
|
|
901
|
+
"generate_structured_response",
|
|
902
|
+
RunnableCallable(
|
|
903
|
+
generate_structured_response,
|
|
904
|
+
agenerate_structured_response,
|
|
905
|
+
),
|
|
906
|
+
)
|
|
907
|
+
if post_model_hook is not None:
|
|
908
|
+
post_model_hook_paths.append("generate_structured_response")
|
|
909
|
+
else:
|
|
910
|
+
agent_paths.append("generate_structured_response")
|
|
911
|
+
else:
|
|
912
|
+
if post_model_hook is not None:
|
|
913
|
+
post_model_hook_paths.append(END)
|
|
914
|
+
else:
|
|
915
|
+
agent_paths.append(END)
|
|
916
|
+
|
|
917
|
+
if post_model_hook is not None:
|
|
918
|
+
|
|
919
|
+
def post_model_hook_router(state: StateSchema) -> str | list[Send]:
|
|
920
|
+
"""Route to the next node after post_model_hook.
|
|
921
|
+
|
|
922
|
+
Routes to one of:
|
|
923
|
+
* "tools": if there are pending tool calls without a corresponding message.
|
|
924
|
+
* "generate_structured_response": if no pending tool calls exist and response_format is specified.
|
|
925
|
+
* END: if no pending tool calls exist and no response_format is specified.
|
|
926
|
+
"""
|
|
927
|
+
|
|
928
|
+
messages = _get_state_value(state, "messages")
|
|
929
|
+
tool_messages = [
|
|
930
|
+
m.tool_call_id for m in messages if isinstance(m, ToolMessage)
|
|
931
|
+
]
|
|
932
|
+
last_ai_message = next(
|
|
933
|
+
m for m in reversed(messages) if isinstance(m, AIMessage)
|
|
934
|
+
)
|
|
935
|
+
pending_tool_calls = [
|
|
936
|
+
c for c in last_ai_message.tool_calls if c["id"] not in tool_messages
|
|
937
|
+
]
|
|
938
|
+
|
|
939
|
+
if pending_tool_calls:
|
|
940
|
+
return [
|
|
941
|
+
Send(
|
|
942
|
+
"tools",
|
|
943
|
+
ToolCallWithContext(
|
|
944
|
+
__type="tool_call_with_context",
|
|
945
|
+
tool_call=call,
|
|
946
|
+
state=state,
|
|
947
|
+
),
|
|
948
|
+
)
|
|
949
|
+
for call in pending_tool_calls
|
|
950
|
+
]
|
|
951
|
+
elif isinstance(messages[-1], ToolMessage):
|
|
952
|
+
return entrypoint
|
|
953
|
+
elif response_format is not None:
|
|
954
|
+
return "generate_structured_response"
|
|
955
|
+
else:
|
|
956
|
+
return END
|
|
957
|
+
|
|
958
|
+
workflow.add_conditional_edges(
|
|
959
|
+
"post_model_hook",
|
|
960
|
+
post_model_hook_router,
|
|
961
|
+
path_map=post_model_hook_paths,
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
workflow.add_conditional_edges(
|
|
965
|
+
"agent",
|
|
966
|
+
should_continue,
|
|
967
|
+
path_map=agent_paths,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
def route_tool_responses(state: StateSchema) -> str:
|
|
971
|
+
for m in reversed(_get_state_value(state, "messages")):
|
|
972
|
+
if not isinstance(m, ToolMessage):
|
|
973
|
+
break
|
|
974
|
+
if m.name in should_return_direct:
|
|
975
|
+
return END
|
|
976
|
+
|
|
977
|
+
# handle a case of parallel tool calls where
|
|
978
|
+
# the tool w/ `return_direct` was executed in a different `Send`
|
|
979
|
+
if isinstance(m, AIMessage) and m.tool_calls:
|
|
980
|
+
if any(call["name"] in should_return_direct for call in m.tool_calls):
|
|
981
|
+
return END
|
|
982
|
+
|
|
983
|
+
return entrypoint
|
|
984
|
+
|
|
985
|
+
if should_return_direct:
|
|
986
|
+
workflow.add_conditional_edges(
|
|
987
|
+
"tools", route_tool_responses, path_map=[entrypoint, END]
|
|
988
|
+
)
|
|
989
|
+
else:
|
|
990
|
+
workflow.add_edge("tools", entrypoint)
|
|
991
|
+
|
|
992
|
+
# Finally, we compile it!
|
|
993
|
+
# This compiles it into a Coco Runnable,
|
|
994
|
+
# meaning you can use it as you would any other runnable
|
|
995
|
+
return workflow.compile(
|
|
996
|
+
checkpointer=checkpointer,
|
|
997
|
+
store=store,
|
|
998
|
+
interrupt_before=interrupt_before,
|
|
999
|
+
interrupt_after=interrupt_after,
|
|
1000
|
+
debug=debug,
|
|
1001
|
+
name=name,
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
# Keep for backwards compatibility
|
|
1006
|
+
create_tool_calling_executor = create_react_agent
|
|
1007
|
+
|
|
1008
|
+
__all__ = [
|
|
1009
|
+
"create_react_agent",
|
|
1010
|
+
"create_tool_calling_executor",
|
|
1011
|
+
"AgentState",
|
|
1012
|
+
"AgentStatePydantic",
|
|
1013
|
+
"AgentStateWithStructuredResponse",
|
|
1014
|
+
"AgentStateWithStructuredResponsePydantic",
|
|
1015
|
+
]
|