utim-cli 2.3.14__tar.gz → 2.3.15__tar.gz
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.
- {utim_cli-2.3.14 → utim_cli-2.3.15}/CHANGELOG.md +5 -0
- {utim_cli-2.3.14/utim_cli.egg-info → utim_cli-2.3.15}/PKG-INFO +1 -1
- {utim_cli-2.3.14 → utim_cli-2.3.15}/pyproject.toml +1 -1
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim/__init__.py +1 -1
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/agent.py +7 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/crawler/markdown_distiller.py +258 -251
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/orchestrator_llm_transport.py +43 -2
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/orchestrator_turn_history.py +40 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/plugins/manager.py +135 -43
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/cli_auth.py +6 -1
- utim_cli-2.3.15/utim_cli/server/email_utils.py +911 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/key_rotator.py +1055 -710
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tools.py +58 -4
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/feedback_dialog.py +87 -14
- utim_cli-2.3.15/utim_cli/tui/plugins_dialog.py +756 -0
- utim_cli-2.3.15/utim_cli/update_checker.py +545 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/utim.py +106 -3
- {utim_cli-2.3.14 → utim_cli-2.3.15/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli.egg-info/SOURCES.txt +1 -0
- utim_cli-2.3.14/utim_cli/server/email_utils.py +0 -691
- utim_cli-2.3.14/utim_cli/tui/plugins_dialog.py +0 -351
- {utim_cli-2.3.14 → utim_cli-2.3.15}/LICENSE +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/MANIFEST.in +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/README.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/setup.cfg +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/setup.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/config.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/orchestrator.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/orchestrator_tool_exec.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/personalities.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/report.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/concurrency.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/db.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/models.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/router.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/share.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/state.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/personality_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.14 → utim_cli-2.3.15}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.15
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: Emend AI Proprietary EULA
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -14,7 +14,7 @@ Quickstart:
|
|
|
14
14
|
try:
|
|
15
15
|
from utim_cli._version import VERSION as __version__
|
|
16
16
|
except ImportError:
|
|
17
|
-
__version__ = "2.3.
|
|
17
|
+
__version__ = "2.3.15"
|
|
18
18
|
|
|
19
19
|
# Developer-friendly core classes (similar to `import genai` / `import openai`)
|
|
20
20
|
from utim_cli.sdk.session import UtimSession as Session
|
|
@@ -168,6 +168,13 @@ class ReActAgent:
|
|
|
168
168
|
arguments[key] = val
|
|
169
169
|
tool_name = actual_name
|
|
170
170
|
|
|
171
|
+
# Collapse duplicated tool names (e.g. 'read_fileread_file', 'read_file read_file',
|
|
172
|
+
# 'run_commandrun_command') produced by buggy model/proxy tool-call serialization.
|
|
173
|
+
import re as _re_dup
|
|
174
|
+
_dup_match = _re_dup.match(r'^([a-zA-Z_]\w*)[\s_]*\1$', tool_name)
|
|
175
|
+
if _dup_match:
|
|
176
|
+
tool_name = _dup_match.group(1)
|
|
177
|
+
|
|
171
178
|
# Map common alias tool names to actual UTIM CLI tool names
|
|
172
179
|
_TOOL_NAME_ALIASES = {
|
|
173
180
|
"shell": "run_command",
|
|
@@ -1,251 +1,258 @@
|
|
|
1
|
-
"""Semantic Markdown Distiller for UTIM HyperCrawler.
|
|
2
|
-
|
|
3
|
-
Transforms raw HTML documents into dense, structured, noise-free Markdown.
|
|
4
|
-
Preserves:
|
|
5
|
-
- Tables (converted to standard GitHub-Flavored Markdown tables)
|
|
6
|
-
- Code blocks (with syntax language identifiers)
|
|
7
|
-
- Headings (#, ##, ###, etc.)
|
|
8
|
-
- Lists (ordered and unordered)
|
|
9
|
-
- Emphasis (bold, italics, inline code)
|
|
10
|
-
- Main article content (stripping navbars, footers, cookie banners, ads, SVGs)
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import re
|
|
16
|
-
import html as html_lib
|
|
17
|
-
from typing import Optional, Tuple, List
|
|
18
|
-
from bs4 import BeautifulSoup, Comment, NavigableString, Tag
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Tags to remove entirely (including their contents)
|
|
22
|
-
_NOISE_TAGS = {
|
|
23
|
-
"script", "style", "nav", "footer", "header", "aside", "noscript",
|
|
24
|
-
"iframe", "svg", "canvas", "form", "button", "dialog", "menu",
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
# Common noise classes / IDs in modern web design
|
|
28
|
-
_NOISE_CLASS_PATTERNS = re.compile(
|
|
29
|
-
r"(ad[s\-_]|banner|cookie|consent|popup|modal|sidebar|newsletter|social|share|widget|tracking|promo)",
|
|
30
|
-
re.IGNORECASE,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class SemanticMarkdownDistiller:
|
|
35
|
-
"""Converts HTML into token-efficient, highly-structured Markdown for LLMs."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, max_length: int = 40000):
|
|
38
|
-
self.max_length = max_length
|
|
39
|
-
|
|
40
|
-
def distill(self, html_content: str, base_url: str = "") -> Tuple[str, str]:
|
|
41
|
-
"""Distills raw HTML into (clean_markdown, page_title)."""
|
|
42
|
-
if not html_content or not html_content.strip():
|
|
43
|
-
return "", ""
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
soup = BeautifulSoup(html_content, "html.parser")
|
|
47
|
-
except Exception:
|
|
48
|
-
# Fallback simple regex extraction if parser fails
|
|
49
|
-
text = re.sub(r"<[^>]+>", " ", html_content)
|
|
50
|
-
return html_lib.unescape(text).strip()[:self.max_length], ""
|
|
51
|
-
|
|
52
|
-
# 1. Extract Page Title
|
|
53
|
-
title = ""
|
|
54
|
-
title_el = soup.find("title") or soup.find("h1")
|
|
55
|
-
if title_el:
|
|
56
|
-
title = title_el.get_text(strip=True)
|
|
57
|
-
|
|
58
|
-
# 2. Strip comments and unwanted noise elements
|
|
59
|
-
for comment in soup.find_all(string=lambda s: isinstance(s, Comment)):
|
|
60
|
-
comment.extract()
|
|
61
|
-
|
|
62
|
-
for tag_name in _NOISE_TAGS:
|
|
63
|
-
for el in soup.find_all(tag_name):
|
|
64
|
-
el.decompose()
|
|
65
|
-
|
|
66
|
-
# 3. Strip obvious ad and cookie banner containers
|
|
67
|
-
for el in soup.find_all(["div", "section", "aside"]):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
inner =
|
|
119
|
-
if inner:
|
|
120
|
-
return f"\n\n{inner}\n\n"
|
|
121
|
-
return ""
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
if tag
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return f"
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
if tag == "
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
]
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1
|
+
"""Semantic Markdown Distiller for UTIM HyperCrawler.
|
|
2
|
+
|
|
3
|
+
Transforms raw HTML documents into dense, structured, noise-free Markdown.
|
|
4
|
+
Preserves:
|
|
5
|
+
- Tables (converted to standard GitHub-Flavored Markdown tables)
|
|
6
|
+
- Code blocks (with syntax language identifiers)
|
|
7
|
+
- Headings (#, ##, ###, etc.)
|
|
8
|
+
- Lists (ordered and unordered)
|
|
9
|
+
- Emphasis (bold, italics, inline code)
|
|
10
|
+
- Main article content (stripping navbars, footers, cookie banners, ads, SVGs)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import html as html_lib
|
|
17
|
+
from typing import Optional, Tuple, List
|
|
18
|
+
from bs4 import BeautifulSoup, Comment, NavigableString, Tag
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Tags to remove entirely (including their contents)
|
|
22
|
+
_NOISE_TAGS = {
|
|
23
|
+
"script", "style", "nav", "footer", "header", "aside", "noscript",
|
|
24
|
+
"iframe", "svg", "canvas", "form", "button", "dialog", "menu",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Common noise classes / IDs in modern web design
|
|
28
|
+
_NOISE_CLASS_PATTERNS = re.compile(
|
|
29
|
+
r"(ad[s\-_]|banner|cookie|consent|popup|modal|sidebar|newsletter|social|share|widget|tracking|promo)",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SemanticMarkdownDistiller:
|
|
35
|
+
"""Converts HTML into token-efficient, highly-structured Markdown for LLMs."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, max_length: int = 40000):
|
|
38
|
+
self.max_length = max_length
|
|
39
|
+
|
|
40
|
+
def distill(self, html_content: str, base_url: str = "") -> Tuple[str, str]:
|
|
41
|
+
"""Distills raw HTML into (clean_markdown, page_title)."""
|
|
42
|
+
if not html_content or not html_content.strip():
|
|
43
|
+
return "", ""
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
47
|
+
except Exception:
|
|
48
|
+
# Fallback simple regex extraction if parser fails
|
|
49
|
+
text = re.sub(r"<[^>]+>", " ", html_content)
|
|
50
|
+
return html_lib.unescape(text).strip()[:self.max_length], ""
|
|
51
|
+
|
|
52
|
+
# 1. Extract Page Title
|
|
53
|
+
title = ""
|
|
54
|
+
title_el = soup.find("title") or soup.find("h1")
|
|
55
|
+
if title_el:
|
|
56
|
+
title = title_el.get_text(strip=True)
|
|
57
|
+
|
|
58
|
+
# 2. Strip comments and unwanted noise elements
|
|
59
|
+
for comment in soup.find_all(string=lambda s: isinstance(s, Comment)):
|
|
60
|
+
comment.extract()
|
|
61
|
+
|
|
62
|
+
for tag_name in _NOISE_TAGS:
|
|
63
|
+
for el in soup.find_all(tag_name):
|
|
64
|
+
el.decompose()
|
|
65
|
+
|
|
66
|
+
# 3. Strip obvious ad and cookie banner containers
|
|
67
|
+
for el in soup.find_all(["div", "section", "aside"]):
|
|
68
|
+
# Defensive: BeautifulSoup can yield tags with attrs=None for
|
|
69
|
+
# malformed/void elements. Guard before calling .get() so a single
|
|
70
|
+
# bad tag (common in GitHub/React-rendered HTML) doesn't kill the
|
|
71
|
+
# entire distillation pass with AttributeError: 'NoneType' has no
|
|
72
|
+
# attribute 'get'.
|
|
73
|
+
if not el.attrs:
|
|
74
|
+
continue
|
|
75
|
+
classes = " ".join(el.get("class", [])) if el.get("class") else ""
|
|
76
|
+
el_id = el.get("id", "")
|
|
77
|
+
if _NOISE_CLASS_PATTERNS.search(classes) or _NOISE_CLASS_PATTERNS.search(el_id):
|
|
78
|
+
# Only decompose if it doesn't contain main article/content
|
|
79
|
+
if not any(k in (classes + el_id).lower() for k in ("article", "content", "main", "post", "entry")):
|
|
80
|
+
el.decompose()
|
|
81
|
+
|
|
82
|
+
# 4. Target the main content container if present
|
|
83
|
+
main_content = (
|
|
84
|
+
soup.find("main")
|
|
85
|
+
or soup.find("article")
|
|
86
|
+
or soup.find("div", {"id": re.compile(r"(content|main|article|docs|post)", re.I)})
|
|
87
|
+
or soup.find("div", {"class": re.compile(r"(content|main|article|docs|post-body)", re.I)})
|
|
88
|
+
or soup.body
|
|
89
|
+
or soup
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# 5. Recursively convert DOM tree to Markdown
|
|
93
|
+
md_text = self._convert_element(main_content).strip()
|
|
94
|
+
|
|
95
|
+
# 6. Normalize whitespace and blank lines
|
|
96
|
+
md_text = re.sub(r"\n{3,}", "\n\n", md_text)
|
|
97
|
+
|
|
98
|
+
if len(md_text) > self.max_length:
|
|
99
|
+
md_text = md_text[:self.max_length] + "\n\n... [Content truncated for length] ..."
|
|
100
|
+
|
|
101
|
+
return md_text, title
|
|
102
|
+
|
|
103
|
+
def _convert_element(self, element) -> str:
|
|
104
|
+
"""Recursively converts a BeautifulSoup Tag into Markdown."""
|
|
105
|
+
if element is None:
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
if isinstance(element, NavigableString):
|
|
109
|
+
text = str(element)
|
|
110
|
+
# Normalize whitespace within inline text
|
|
111
|
+
return re.sub(r"[ \t\r\f\v]+", " ", text)
|
|
112
|
+
|
|
113
|
+
tag = element.name.lower() if element.name else ""
|
|
114
|
+
|
|
115
|
+
# Headings
|
|
116
|
+
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
117
|
+
level = int(tag[1])
|
|
118
|
+
inner = self._get_inner_text(element).strip()
|
|
119
|
+
if inner:
|
|
120
|
+
return f"\n\n{'#' * level} {inner}\n\n"
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
# Paragraphs & Blocks
|
|
124
|
+
if tag in ("p", "div", "section", "article"):
|
|
125
|
+
inner = "".join(self._convert_element(c) for c in element.children).strip()
|
|
126
|
+
if inner:
|
|
127
|
+
return f"\n\n{inner}\n\n"
|
|
128
|
+
return ""
|
|
129
|
+
|
|
130
|
+
# Code Blocks (<pre><code> or <pre>)
|
|
131
|
+
if tag == "pre":
|
|
132
|
+
code_el = element.find("code")
|
|
133
|
+
lang = ""
|
|
134
|
+
if code_el and code_el.get("class"):
|
|
135
|
+
for c in code_el["class"]:
|
|
136
|
+
if c.startswith("language-") or c.startswith("lang-"):
|
|
137
|
+
lang = c.split("-", 1)[1]
|
|
138
|
+
break
|
|
139
|
+
code_text = (code_el or element).get_text()
|
|
140
|
+
return f"\n\n```{lang}\n{code_text.rstrip()}\n```\n\n"
|
|
141
|
+
|
|
142
|
+
# Inline Code (<code>)
|
|
143
|
+
if tag == "code":
|
|
144
|
+
code_text = element.get_text()
|
|
145
|
+
if "\n" in code_text:
|
|
146
|
+
return f"\n\n```\n{code_text.rstrip()}\n```\n\n"
|
|
147
|
+
return f"`{code_text.strip()}`"
|
|
148
|
+
|
|
149
|
+
# Blockquote
|
|
150
|
+
if tag == "blockquote":
|
|
151
|
+
inner = "".join(self._convert_element(c) for c in element.children).strip()
|
|
152
|
+
lines = [f"> {l}" for l in inner.splitlines()]
|
|
153
|
+
return "\n\n" + "\n".join(lines) + "\n\n"
|
|
154
|
+
|
|
155
|
+
# Lists (<ul>, <ol>)
|
|
156
|
+
if tag in ("ul", "ol"):
|
|
157
|
+
items = []
|
|
158
|
+
is_ordered = (tag == "ol")
|
|
159
|
+
for idx, li in enumerate(element.find_all("li", recursive=False)):
|
|
160
|
+
prefix = f"{idx + 1}. " if is_ordered else "- "
|
|
161
|
+
li_text = "".join(self._convert_element(c) for c in li.children).strip()
|
|
162
|
+
if li_text:
|
|
163
|
+
items.append(f"{prefix}{li_text}")
|
|
164
|
+
if items:
|
|
165
|
+
return "\n\n" + "\n".join(items) + "\n\n"
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
# List item
|
|
169
|
+
if tag == "li":
|
|
170
|
+
return "".join(self._convert_element(c) for c in element.children).strip()
|
|
171
|
+
|
|
172
|
+
# Tables (<table>) -> Markdown Table
|
|
173
|
+
if tag == "table":
|
|
174
|
+
return self._convert_table(element)
|
|
175
|
+
|
|
176
|
+
# Emphasis & Formatting
|
|
177
|
+
if tag in ("strong", "b"):
|
|
178
|
+
inner = "".join(self._convert_element(c) for c in element.children).strip()
|
|
179
|
+
return f"**{inner}**" if inner else ""
|
|
180
|
+
|
|
181
|
+
if tag in ("em", "i"):
|
|
182
|
+
inner = "".join(self._convert_element(c) for c in element.children).strip()
|
|
183
|
+
return f"*{inner}*" if inner else ""
|
|
184
|
+
|
|
185
|
+
# Links (<a>)
|
|
186
|
+
if tag == "a":
|
|
187
|
+
href = element.get("href", "").strip()
|
|
188
|
+
inner = "".join(self._convert_element(c) for c in element.children).strip()
|
|
189
|
+
if href and inner and not href.startswith("javascript:") and not href.startswith("#"):
|
|
190
|
+
return f"[{inner}]({href})"
|
|
191
|
+
return inner
|
|
192
|
+
|
|
193
|
+
# Images (<img>)
|
|
194
|
+
if tag == "img":
|
|
195
|
+
alt = element.get("alt", "").strip()
|
|
196
|
+
src = element.get("src", "").strip()
|
|
197
|
+
if src and not src.startswith("data:"):
|
|
198
|
+
return f""
|
|
199
|
+
return f"[{alt}]" if alt else ""
|
|
200
|
+
|
|
201
|
+
# Line Break (<br>, <hr>)
|
|
202
|
+
if tag == "br":
|
|
203
|
+
return "\n"
|
|
204
|
+
if tag == "hr":
|
|
205
|
+
return "\n\n---\n\n"
|
|
206
|
+
|
|
207
|
+
# Default: traverse children
|
|
208
|
+
return "".join(self._convert_element(c) for c in element.children)
|
|
209
|
+
|
|
210
|
+
def _get_inner_text(self, tag: Tag) -> str:
|
|
211
|
+
"""Extracts clean text without extra formatting."""
|
|
212
|
+
return tag.get_text(separator=" ", strip=True)
|
|
213
|
+
|
|
214
|
+
def _convert_table(self, table_tag: Tag) -> str:
|
|
215
|
+
"""Converts HTML table into a clean GitHub Flavored Markdown table."""
|
|
216
|
+
rows = table_tag.find_all("tr")
|
|
217
|
+
if not rows:
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
table_matrix: List[List[str]] = []
|
|
221
|
+
for r in rows:
|
|
222
|
+
cols = r.find_all(["th", "td"])
|
|
223
|
+
col_texts = []
|
|
224
|
+
for c in cols:
|
|
225
|
+
# Get text and clean pipe characters to prevent markdown table breakage
|
|
226
|
+
c_text = "".join(self._convert_element(child) for child in c.children).strip()
|
|
227
|
+
c_text = c_text.replace("\n", " ").replace("|", "\\|")
|
|
228
|
+
col_texts.append(c_text)
|
|
229
|
+
if any(col_texts):
|
|
230
|
+
table_matrix.append(col_texts)
|
|
231
|
+
|
|
232
|
+
if not table_matrix:
|
|
233
|
+
return ""
|
|
234
|
+
|
|
235
|
+
# Normalize column widths
|
|
236
|
+
max_cols = max(len(r) for r in table_matrix)
|
|
237
|
+
for r in table_matrix:
|
|
238
|
+
while len(r) < max_cols:
|
|
239
|
+
r.append("")
|
|
240
|
+
|
|
241
|
+
header_row = table_matrix[0]
|
|
242
|
+
separator_row = ["---"] * max_cols
|
|
243
|
+
body_rows = table_matrix[1:] if len(table_matrix) > 1 else []
|
|
244
|
+
|
|
245
|
+
lines = [
|
|
246
|
+
"| " + " | ".join(header_row) + " |",
|
|
247
|
+
"| " + " | ".join(separator_row) + " |",
|
|
248
|
+
]
|
|
249
|
+
for r in body_rows:
|
|
250
|
+
lines.append("| " + " | ".join(r) + " |")
|
|
251
|
+
|
|
252
|
+
return "\n\n" + "\n".join(lines) + "\n\n"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def html_to_markdown(html_content: str, base_url: str = "", max_length: int = 40000) -> Tuple[str, str]:
|
|
256
|
+
"""Convenience function to distill HTML into clean Markdown."""
|
|
257
|
+
distiller = SemanticMarkdownDistiller(max_length=max_length)
|
|
258
|
+
return distiller.distill(html_content, base_url=base_url)
|