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
coco_core/utils/html.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Utilities for working with HTML."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from urllib.parse import urljoin, urlparse
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
PREFIXES_TO_IGNORE = ("javascript:", "mailto:", "#")
|
|
11
|
+
|
|
12
|
+
SUFFIXES_TO_IGNORE = (
|
|
13
|
+
".css",
|
|
14
|
+
".js",
|
|
15
|
+
".ico",
|
|
16
|
+
".png",
|
|
17
|
+
".jpg",
|
|
18
|
+
".jpeg",
|
|
19
|
+
".gif",
|
|
20
|
+
".svg",
|
|
21
|
+
".csv",
|
|
22
|
+
".bz2",
|
|
23
|
+
".zip",
|
|
24
|
+
".epub",
|
|
25
|
+
".webp",
|
|
26
|
+
".pdf",
|
|
27
|
+
".docx",
|
|
28
|
+
".xlsx",
|
|
29
|
+
".pptx",
|
|
30
|
+
".pptm",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
SUFFIXES_TO_IGNORE_REGEX = (
|
|
34
|
+
"(?!" + "|".join([re.escape(s) + r"[\#'\"]" for s in SUFFIXES_TO_IGNORE]) + ")"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
PREFIXES_TO_IGNORE_REGEX = (
|
|
38
|
+
"(?!" + "|".join([re.escape(s) for s in PREFIXES_TO_IGNORE]) + ")"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
DEFAULT_LINK_REGEX = (
|
|
42
|
+
rf"href=[\"']{PREFIXES_TO_IGNORE_REGEX}((?:{SUFFIXES_TO_IGNORE_REGEX}.)*?)[\#'\"]"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_all_links(
|
|
47
|
+
raw_html: str, *, pattern: str | re.Pattern[str] | None = None
|
|
48
|
+
) -> list[str]:
|
|
49
|
+
"""Extract all links from a raw HTML string.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
raw_html: original HTML.
|
|
53
|
+
pattern: Regex to use for extracting links from raw HTML.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A list of all links found in the HTML.
|
|
57
|
+
"""
|
|
58
|
+
pattern = pattern or DEFAULT_LINK_REGEX
|
|
59
|
+
return list(set(re.findall(pattern, raw_html)))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_sub_links(
|
|
63
|
+
raw_html: str,
|
|
64
|
+
url: str,
|
|
65
|
+
*,
|
|
66
|
+
base_url: str | None = None,
|
|
67
|
+
pattern: str | re.Pattern[str] | None = None,
|
|
68
|
+
prevent_outside: bool = True,
|
|
69
|
+
exclude_prefixes: Sequence[str] = (),
|
|
70
|
+
continue_on_failure: bool = False,
|
|
71
|
+
) -> list[str]:
|
|
72
|
+
"""Extract all links from a raw HTML string and convert into absolute paths.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
raw_html: Original HTML.
|
|
76
|
+
url: The url of the HTML.
|
|
77
|
+
base_url: the base URL to check for outside links against.
|
|
78
|
+
pattern: Regex to use for extracting links from raw HTML.
|
|
79
|
+
prevent_outside: If `True`, ignore external links which are not children
|
|
80
|
+
of the base URL.
|
|
81
|
+
exclude_prefixes: Exclude any URLs that start with one of these prefixes.
|
|
82
|
+
continue_on_failure: If `True`, continue if parsing a specific link raises an
|
|
83
|
+
exception. Otherwise, raise the exception.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A list of absolute paths to sub links.
|
|
87
|
+
"""
|
|
88
|
+
base_url_to_use = base_url if base_url is not None else url
|
|
89
|
+
parsed_base_url = urlparse(base_url_to_use)
|
|
90
|
+
parsed_url = urlparse(url)
|
|
91
|
+
all_links = find_all_links(raw_html, pattern=pattern)
|
|
92
|
+
absolute_paths = set()
|
|
93
|
+
for link in all_links:
|
|
94
|
+
try:
|
|
95
|
+
parsed_link = urlparse(link)
|
|
96
|
+
# Some may be absolute links like https://to/path
|
|
97
|
+
if parsed_link.scheme in {"http", "https"}:
|
|
98
|
+
absolute_path = link
|
|
99
|
+
# Some may have omitted the protocol like //to/path
|
|
100
|
+
elif link.startswith("//"):
|
|
101
|
+
absolute_path = f"{parsed_url.scheme}:{link}"
|
|
102
|
+
else:
|
|
103
|
+
absolute_path = urljoin(url, parsed_link.path)
|
|
104
|
+
if parsed_link.query:
|
|
105
|
+
absolute_path += f"?{parsed_link.query}"
|
|
106
|
+
absolute_paths.add(absolute_path)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
if continue_on_failure:
|
|
109
|
+
logger.warning(
|
|
110
|
+
"Unable to load link %s. Raised exception:\n\n%s", link, e
|
|
111
|
+
)
|
|
112
|
+
continue
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
results = []
|
|
116
|
+
for path in absolute_paths:
|
|
117
|
+
if any(path.startswith(exclude_prefix) for exclude_prefix in exclude_prefixes):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if prevent_outside:
|
|
121
|
+
parsed_path = urlparse(path)
|
|
122
|
+
|
|
123
|
+
if parsed_base_url.netloc != parsed_path.netloc:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Will take care of verifying rest of path after netloc
|
|
127
|
+
# if it's more specific
|
|
128
|
+
if not path.startswith(base_url_to_use):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
results.append(path)
|
|
132
|
+
return results
|
coco_core/utils/image.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Utilities for image processing."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name: str) -> Any:
|
|
7
|
+
if name in {"encode_image", "image_to_data_url"}:
|
|
8
|
+
msg = (
|
|
9
|
+
f"'{name}' has been removed for security reasons.\n\n"
|
|
10
|
+
f"Usage of this utility in environments with user-input paths is a "
|
|
11
|
+
f"security vulnerability. Out of an abundance of caution, the utility "
|
|
12
|
+
f"has been removed to prevent possible misuse."
|
|
13
|
+
)
|
|
14
|
+
raise ValueError(msg)
|
|
15
|
+
raise AttributeError(name)
|
coco_core/utils/input.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Handle chained inputs."""
|
|
2
|
+
|
|
3
|
+
from typing import TextIO
|
|
4
|
+
|
|
5
|
+
_TEXT_COLOR_MAPPING = {
|
|
6
|
+
"blue": "36;1",
|
|
7
|
+
"yellow": "33;1",
|
|
8
|
+
"pink": "38;5;200",
|
|
9
|
+
"green": "32;1",
|
|
10
|
+
"red": "31;1",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_color_mapping(
|
|
15
|
+
items: list[str], excluded_colors: list[str] | None = None
|
|
16
|
+
) -> dict[str, str]:
|
|
17
|
+
"""Get mapping for items to a support color.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
items: The items to map to colors.
|
|
21
|
+
excluded_colors: The colors to exclude.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The mapping of items to colors.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If no colors are available after applying exclusions.
|
|
28
|
+
"""
|
|
29
|
+
colors = list(_TEXT_COLOR_MAPPING.keys())
|
|
30
|
+
if excluded_colors is not None:
|
|
31
|
+
colors = [c for c in colors if c not in excluded_colors]
|
|
32
|
+
if not colors:
|
|
33
|
+
msg = "No colors available after applying exclusions."
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
return {item: colors[i % len(colors)] for i, item in enumerate(items)}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_colored_text(text: str, color: str) -> str:
|
|
39
|
+
"""Get colored text.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
text: The text to color.
|
|
43
|
+
color: The color to use.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The colored text.
|
|
47
|
+
"""
|
|
48
|
+
color_str = _TEXT_COLOR_MAPPING[color]
|
|
49
|
+
return f"\u001b[{color_str}m\033[1;3m{text}\u001b[0m"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_bolded_text(text: str) -> str:
|
|
53
|
+
"""Get bolded text.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
text: The text to bold.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The bolded text.
|
|
60
|
+
"""
|
|
61
|
+
return f"\033[1m{text}\033[0m"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def print_text(
|
|
65
|
+
text: str, color: str | None = None, end: str = "", file: TextIO | None = None
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Print text with highlighting and no end characters.
|
|
68
|
+
|
|
69
|
+
If a color is provided, the text will be printed in that color.
|
|
70
|
+
|
|
71
|
+
If a file is provided, the text will be written to that file.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
text: The text to print.
|
|
75
|
+
color: The color to use.
|
|
76
|
+
end: The end character to use.
|
|
77
|
+
file: The file to write to.
|
|
78
|
+
"""
|
|
79
|
+
text_to_print = get_colored_text(text, color) if color else text
|
|
80
|
+
print(text_to_print, end=end, file=file)
|
|
81
|
+
if file:
|
|
82
|
+
file.flush() # ensure all printed content are written to file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Utilities for working with interactive environments."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_interactive_env() -> bool:
|
|
7
|
+
"""Determine if running within IPython or Jupyter.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
`True` if running in an interactive environment, `False` otherwise.
|
|
11
|
+
"""
|
|
12
|
+
return hasattr(sys, "ps2")
|
coco_core/utils/iter.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Utilities for working with iterators."""
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Generator, Iterable, Iterator
|
|
5
|
+
from contextlib import AbstractContextManager
|
|
6
|
+
from itertools import islice
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Generic,
|
|
11
|
+
Literal,
|
|
12
|
+
TypeVar,
|
|
13
|
+
overload,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NoLock:
|
|
20
|
+
"""Dummy lock that provides the proper interface but no protection."""
|
|
21
|
+
|
|
22
|
+
def __enter__(self) -> None:
|
|
23
|
+
"""Do nothing."""
|
|
24
|
+
|
|
25
|
+
def __exit__(
|
|
26
|
+
self,
|
|
27
|
+
exc_type: type[BaseException] | None,
|
|
28
|
+
exc_val: BaseException | None,
|
|
29
|
+
exc_tb: TracebackType | None,
|
|
30
|
+
) -> Literal[False]:
|
|
31
|
+
"""Return False (exception not suppressed)."""
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def tee_peer(
|
|
36
|
+
iterator: Iterator[T],
|
|
37
|
+
# the buffer specific to this peer
|
|
38
|
+
buffer: deque[T],
|
|
39
|
+
# the buffers of all peers, including our own
|
|
40
|
+
peers: list[deque[T]],
|
|
41
|
+
lock: AbstractContextManager[Any],
|
|
42
|
+
) -> Generator[T, None, None]:
|
|
43
|
+
"""An individual iterator of a `.tee`.
|
|
44
|
+
|
|
45
|
+
This function is a generator that yields items from the shared iterator `iterator`.
|
|
46
|
+
It buffers items until the least advanced iterator has yielded them as well. The
|
|
47
|
+
buffer is shared with all other peers.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
iterator: The shared iterator.
|
|
51
|
+
buffer: The buffer for this peer.
|
|
52
|
+
peers: The buffers of all peers.
|
|
53
|
+
lock: The lock to synchronise access to the shared buffers.
|
|
54
|
+
|
|
55
|
+
Yields:
|
|
56
|
+
The next item from the shared iterator.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
while True:
|
|
60
|
+
if not buffer:
|
|
61
|
+
with lock:
|
|
62
|
+
# Another peer produced an item while we were waiting for the lock.
|
|
63
|
+
# Proceed with the next loop iteration to yield the item.
|
|
64
|
+
if buffer:
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
item = next(iterator)
|
|
68
|
+
except StopIteration:
|
|
69
|
+
break
|
|
70
|
+
else:
|
|
71
|
+
# Append to all buffers, including our own. We'll fetch our
|
|
72
|
+
# item from the buffer again, instead of yielding it directly.
|
|
73
|
+
# This ensures the proper item ordering if any of our peers
|
|
74
|
+
# are fetching items concurrently. They may have buffered their
|
|
75
|
+
# item already.
|
|
76
|
+
for peer_buffer in peers:
|
|
77
|
+
peer_buffer.append(item)
|
|
78
|
+
yield buffer.popleft()
|
|
79
|
+
finally:
|
|
80
|
+
with lock:
|
|
81
|
+
# this peer is done - remove its buffer
|
|
82
|
+
for idx, peer_buffer in enumerate(peers): # pragma: no branch
|
|
83
|
+
if peer_buffer is buffer:
|
|
84
|
+
peers.pop(idx)
|
|
85
|
+
break
|
|
86
|
+
# if we are the last peer, try and close the iterator
|
|
87
|
+
if not peers and hasattr(iterator, "close"):
|
|
88
|
+
iterator.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Tee(Generic[T]):
|
|
92
|
+
"""Create `n` separate asynchronous iterators over `iterable`.
|
|
93
|
+
|
|
94
|
+
This splits a single `iterable` into multiple iterators, each providing the same
|
|
95
|
+
items in the same order.
|
|
96
|
+
|
|
97
|
+
All child iterators may advance separately but share the same items from `iterable`
|
|
98
|
+
-- when the most advanced iterator retrieves an item, it is buffered until the least
|
|
99
|
+
advanced iterator has yielded it as well. A `tee` works lazily and can handle an
|
|
100
|
+
infinite `iterable`, provided that all iterators advance.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
async def derivative(sensor_data):
|
|
104
|
+
previous, current = a.tee(sensor_data, n=2)
|
|
105
|
+
await a.anext(previous) # advance one iterator
|
|
106
|
+
return a.map(operator.sub, previous, current)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Unlike `itertools.tee`, `.tee` returns a custom type instead of a `tuple`. Like a
|
|
110
|
+
tuple, it can be indexed, iterated and unpacked to get the child iterators. In
|
|
111
|
+
addition, its `.tee.aclose` method immediately closes all children, and it can be
|
|
112
|
+
used in an `async with` context for the same effect.
|
|
113
|
+
|
|
114
|
+
If `iterable` is an iterator and read elsewhere, `tee` will *not* provide these
|
|
115
|
+
items. Also, `tee` must internally buffer each item until the last iterator has
|
|
116
|
+
yielded it; if the most and least advanced iterator differ by most data, using a
|
|
117
|
+
`list` is more efficient (but not lazy).
|
|
118
|
+
|
|
119
|
+
If the underlying iterable is concurrency safe (`anext` may be awaited concurrently)
|
|
120
|
+
the resulting iterators are concurrency safe as well. Otherwise, the iterators are
|
|
121
|
+
safe if there is only ever one single "most advanced" iterator. To enforce
|
|
122
|
+
sequential use of `anext`, provide a `lock`
|
|
123
|
+
|
|
124
|
+
- e.g., an `asyncio.Lock` instance in an `asyncio` application - and access is
|
|
125
|
+
automatically synchronised.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
iterable: Iterator[T],
|
|
132
|
+
n: int = 2,
|
|
133
|
+
*,
|
|
134
|
+
lock: AbstractContextManager[Any] | None = None,
|
|
135
|
+
):
|
|
136
|
+
"""Create a `tee`.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
iterable: The iterable to split.
|
|
140
|
+
n: The number of iterators to create.
|
|
141
|
+
lock: The lock to synchronise access to the shared buffers.
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
self._iterator = iter(iterable)
|
|
145
|
+
self._buffers: list[deque[T]] = [deque() for _ in range(n)]
|
|
146
|
+
self._children = tuple(
|
|
147
|
+
tee_peer(
|
|
148
|
+
iterator=self._iterator,
|
|
149
|
+
buffer=buffer,
|
|
150
|
+
peers=self._buffers,
|
|
151
|
+
lock=lock if lock is not None else NoLock(),
|
|
152
|
+
)
|
|
153
|
+
for buffer in self._buffers
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def __len__(self) -> int:
|
|
157
|
+
"""Return the number of child iterators."""
|
|
158
|
+
return len(self._children)
|
|
159
|
+
|
|
160
|
+
@overload
|
|
161
|
+
def __getitem__(self, item: int) -> Iterator[T]: ...
|
|
162
|
+
|
|
163
|
+
@overload
|
|
164
|
+
def __getitem__(self, item: slice) -> tuple[Iterator[T], ...]: ...
|
|
165
|
+
|
|
166
|
+
def __getitem__(self, item: int | slice) -> Iterator[T] | tuple[Iterator[T], ...]:
|
|
167
|
+
"""Return the child iterator(s) at the given index or slice."""
|
|
168
|
+
return self._children[item]
|
|
169
|
+
|
|
170
|
+
def __iter__(self) -> Iterator[Iterator[T]]:
|
|
171
|
+
"""Return an iterator over the child iterators.
|
|
172
|
+
|
|
173
|
+
Yields:
|
|
174
|
+
The child iterators.
|
|
175
|
+
"""
|
|
176
|
+
yield from self._children
|
|
177
|
+
|
|
178
|
+
def __enter__(self) -> "Tee[T]":
|
|
179
|
+
"""Return `Tee` instance."""
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def __exit__(
|
|
183
|
+
self,
|
|
184
|
+
exc_type: type[BaseException] | None,
|
|
185
|
+
exc_val: BaseException | None,
|
|
186
|
+
exc_tb: TracebackType | None,
|
|
187
|
+
) -> Literal[False]:
|
|
188
|
+
"""Close all child iterators.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
`False` (exception not suppressed).
|
|
192
|
+
"""
|
|
193
|
+
self.close()
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def close(self) -> None:
|
|
197
|
+
"""Close all child iterators."""
|
|
198
|
+
for child in self._children:
|
|
199
|
+
child.close()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Why this is needed https://stackoverflow.com/a/44638570
|
|
203
|
+
safetee = Tee
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def batch_iterate(size: int | None, iterable: Iterable[T]) -> Iterator[list[T]]:
|
|
207
|
+
"""Utility batching function.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
size: The size of the batch.
|
|
211
|
+
|
|
212
|
+
If `None`, returns a single batch.
|
|
213
|
+
iterable: The iterable to batch.
|
|
214
|
+
|
|
215
|
+
Yields:
|
|
216
|
+
The batches of the iterable.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If `size` is not `None` and is not a positive integer.
|
|
220
|
+
"""
|
|
221
|
+
if size is not None and size <= 0:
|
|
222
|
+
msg = f"Batch size must be a positive integer, got {size}."
|
|
223
|
+
raise ValueError(msg)
|
|
224
|
+
it = iter(iterable)
|
|
225
|
+
while True:
|
|
226
|
+
chunk = list(islice(it, size))
|
|
227
|
+
if not chunk:
|
|
228
|
+
return
|
|
229
|
+
yield chunk
|
coco_core/utils/json.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Utilities for JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from coco_core.exceptions import OutputParserException
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _replace_new_line(match: re.Match[str]) -> str:
|
|
16
|
+
"""Replace newline characters in a regex match with escaped sequences.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
match: Regex match object containing the string to process.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
String with newlines, carriage returns, tabs, and quotes properly escaped.
|
|
23
|
+
"""
|
|
24
|
+
value = match.group(2)
|
|
25
|
+
value = re.sub(r"\n", r"\\n", value)
|
|
26
|
+
value = re.sub(r"\r", r"\\r", value)
|
|
27
|
+
value = re.sub(r"\t", r"\\t", value)
|
|
28
|
+
value = re.sub(r'(?<!\\)"', r"\"", value)
|
|
29
|
+
|
|
30
|
+
return match.group(1) + value + match.group(3)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _custom_parser(multiline_string: str | bytes | bytearray) -> str:
|
|
34
|
+
r"""Custom parser for multiline strings.
|
|
35
|
+
|
|
36
|
+
The LLM response for `action_input` may be a multiline string containing unescaped
|
|
37
|
+
newlines, tabs or quotes. This function replaces those characters with their escaped
|
|
38
|
+
counterparts. (newlines in JSON must be double-escaped: `\\n`).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The modified string with escaped newlines, tabs and quotes.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(multiline_string, (bytes, bytearray)):
|
|
44
|
+
multiline_string = multiline_string.decode()
|
|
45
|
+
|
|
46
|
+
return re.sub(
|
|
47
|
+
r'("action_input"\:\s*")(.*?)(")',
|
|
48
|
+
_replace_new_line,
|
|
49
|
+
multiline_string,
|
|
50
|
+
flags=re.DOTALL,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Adapted from https://github.com/KillianLucas/open-interpreter/blob/5b6080fae1f8c68938a1e4fa8667e3744084ee21/interpreter/utils/parse_partial_json.py
|
|
55
|
+
# MIT License
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_partial_json(s: str, *, strict: bool = False) -> Any:
|
|
59
|
+
"""Parse a JSON string that may be missing closing braces.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
s: The JSON string to parse.
|
|
63
|
+
strict: Whether to use strict parsing.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The parsed JSON object as a Python dictionary.
|
|
67
|
+
"""
|
|
68
|
+
# Attempt to parse the string as-is.
|
|
69
|
+
try:
|
|
70
|
+
return json.loads(s, strict=strict)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
# Initialize variables.
|
|
75
|
+
new_chars = []
|
|
76
|
+
stack = []
|
|
77
|
+
is_inside_string = False
|
|
78
|
+
escaped = False
|
|
79
|
+
|
|
80
|
+
# Process each character in the string one at a time.
|
|
81
|
+
for char in s:
|
|
82
|
+
new_char = char
|
|
83
|
+
if is_inside_string:
|
|
84
|
+
if char == '"' and not escaped:
|
|
85
|
+
is_inside_string = False
|
|
86
|
+
elif char == "\n" and not escaped:
|
|
87
|
+
new_char = (
|
|
88
|
+
"\\n" # Replace the newline character with the escape sequence.
|
|
89
|
+
)
|
|
90
|
+
elif char == "\\":
|
|
91
|
+
escaped = not escaped
|
|
92
|
+
else:
|
|
93
|
+
escaped = False
|
|
94
|
+
elif char == '"':
|
|
95
|
+
is_inside_string = True
|
|
96
|
+
escaped = False
|
|
97
|
+
elif char == "{":
|
|
98
|
+
stack.append("}")
|
|
99
|
+
elif char == "[":
|
|
100
|
+
stack.append("]")
|
|
101
|
+
elif char in {"}", "]"}:
|
|
102
|
+
if stack and stack[-1] == char:
|
|
103
|
+
stack.pop()
|
|
104
|
+
else:
|
|
105
|
+
# Mismatched closing character; the input is malformed.
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
# Append the processed character to the new string.
|
|
109
|
+
new_chars.append(new_char)
|
|
110
|
+
|
|
111
|
+
# If we're still inside a string at the end of processing,
|
|
112
|
+
# we need to close the string.
|
|
113
|
+
if is_inside_string:
|
|
114
|
+
if escaped: # Remove unterminated escape character
|
|
115
|
+
new_chars.pop()
|
|
116
|
+
new_chars.append('"')
|
|
117
|
+
|
|
118
|
+
# Reverse the stack to get the closing characters.
|
|
119
|
+
stack.reverse()
|
|
120
|
+
|
|
121
|
+
# Try to parse mods of string until we succeed or run out of characters.
|
|
122
|
+
while new_chars:
|
|
123
|
+
# Close any remaining open structures in the reverse
|
|
124
|
+
# order that they were opened.
|
|
125
|
+
# Attempt to parse the modified string as JSON.
|
|
126
|
+
try:
|
|
127
|
+
return json.loads("".join(new_chars + stack), strict=strict)
|
|
128
|
+
except json.JSONDecodeError:
|
|
129
|
+
# If we still can't parse the string as JSON,
|
|
130
|
+
# try removing the last character
|
|
131
|
+
new_chars.pop()
|
|
132
|
+
|
|
133
|
+
# If we got here, we ran out of characters to remove
|
|
134
|
+
# and still couldn't parse the string as JSON, so return the parse error
|
|
135
|
+
# for the original string.
|
|
136
|
+
return json.loads(s, strict=strict)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_json_markdown_re = re.compile(r"```(json)?(.*)", re.DOTALL)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_json_markdown(
|
|
143
|
+
json_string: str, *, parser: Callable[[str], Any] = parse_partial_json
|
|
144
|
+
) -> Any:
|
|
145
|
+
"""Parse a JSON string from a Markdown string.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
json_string: The Markdown string.
|
|
149
|
+
parser: The parser to use.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The parsed JSON object as a Python dictionary.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
return _parse_json(json_string, parser=parser)
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
# Try to find JSON string within triple backticks
|
|
158
|
+
match = _json_markdown_re.search(json_string)
|
|
159
|
+
|
|
160
|
+
# If no match found, assume the entire string is a JSON string
|
|
161
|
+
# Else, use the content within the backticks
|
|
162
|
+
json_str = json_string if match is None else match.group(2)
|
|
163
|
+
return _parse_json(json_str, parser=parser)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_json_strip_chars = " \n\r\t`"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _parse_json(
|
|
170
|
+
json_str: str, *, parser: Callable[[str], Any] = parse_partial_json
|
|
171
|
+
) -> Any:
|
|
172
|
+
"""Parse a JSON string, handling special characters and whitespace.
|
|
173
|
+
|
|
174
|
+
Strips whitespace, newlines, and backticks from the start and end of the string,
|
|
175
|
+
then processes special characters before parsing.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
json_str: The JSON string to parse.
|
|
179
|
+
parser: Optional custom parser function.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Parsed JSON object.
|
|
183
|
+
"""
|
|
184
|
+
# Strip whitespace,newlines,backtick from the start and end
|
|
185
|
+
json_str = json_str.strip(_json_strip_chars)
|
|
186
|
+
|
|
187
|
+
# handle newlines and other special characters inside the returned value
|
|
188
|
+
json_str = _custom_parser(json_str)
|
|
189
|
+
|
|
190
|
+
# Parse the JSON string into a Python dictionary
|
|
191
|
+
return parser(json_str)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_and_check_json_markdown(
|
|
195
|
+
text: str, expected_keys: list[str]
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""Parse and check a JSON string from a Markdown string.
|
|
198
|
+
|
|
199
|
+
Checks that it contains the expected keys.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
text: The Markdown string.
|
|
203
|
+
expected_keys: The expected keys in the JSON string.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The parsed JSON object as a Python dictionary.
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
OutputParserException: If the JSON string is invalid or does not contain
|
|
210
|
+
the expected keys.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
json_obj = parse_json_markdown(text)
|
|
214
|
+
except json.JSONDecodeError as e:
|
|
215
|
+
msg = f"Got invalid JSON object. Error: {e}"
|
|
216
|
+
raise OutputParserException(msg) from e
|
|
217
|
+
if not isinstance(json_obj, dict):
|
|
218
|
+
error_message = (
|
|
219
|
+
f"Expected JSON object (dict), but got: {type(json_obj).__name__}. "
|
|
220
|
+
)
|
|
221
|
+
raise OutputParserException(error_message, llm_output=text)
|
|
222
|
+
|
|
223
|
+
for key in expected_keys:
|
|
224
|
+
if key not in json_obj:
|
|
225
|
+
msg = (
|
|
226
|
+
f"Got invalid return object. Expected key `{key}` "
|
|
227
|
+
f"to be present, but got {json_obj}"
|
|
228
|
+
)
|
|
229
|
+
raise OutputParserException(msg)
|
|
230
|
+
return json_obj
|