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