camel-ai 0.2.68__py3-none-any.whl → 0.2.69a1__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.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +170 -11
- camel/configs/vllm_config.py +2 -0
- camel/datagen/self_improving_cot.py +1 -1
- camel/memories/context_creators/score_based.py +129 -87
- camel/runtimes/configs.py +11 -11
- camel/runtimes/daytona_runtime.py +4 -4
- camel/runtimes/docker_runtime.py +6 -6
- camel/runtimes/remote_http_runtime.py +5 -5
- camel/societies/workforce/prompts.py +13 -12
- camel/societies/workforce/single_agent_worker.py +252 -22
- camel/societies/workforce/utils.py +10 -2
- camel/societies/workforce/worker.py +21 -45
- camel/societies/workforce/workforce.py +36 -15
- camel/tasks/task.py +18 -12
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/aci_toolkit.py +19 -19
- camel/toolkits/arxiv_toolkit.py +6 -6
- camel/toolkits/dappier_toolkit.py +5 -5
- camel/toolkits/file_write_toolkit.py +10 -10
- camel/toolkits/github_toolkit.py +3 -3
- camel/toolkits/non_visual_browser_toolkit/__init__.py +18 -0
- camel/toolkits/non_visual_browser_toolkit/actions.py +196 -0
- camel/toolkits/non_visual_browser_toolkit/agent.py +278 -0
- camel/toolkits/non_visual_browser_toolkit/browser_non_visual_toolkit.py +363 -0
- camel/toolkits/non_visual_browser_toolkit/nv_browser_session.py +175 -0
- camel/toolkits/non_visual_browser_toolkit/snapshot.js +188 -0
- camel/toolkits/non_visual_browser_toolkit/snapshot.py +164 -0
- camel/toolkits/pptx_toolkit.py +4 -4
- camel/toolkits/sympy_toolkit.py +1 -1
- camel/toolkits/task_planning_toolkit.py +3 -3
- camel/toolkits/thinking_toolkit.py +1 -1
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/METADATA +1 -1
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/RECORD +36 -29
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.68.dist-info → camel_ai-0.2.69a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
18
|
+
|
|
19
|
+
from .actions import ActionExecutor
|
|
20
|
+
from .snapshot import PageSnapshot
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from playwright.async_api import (
|
|
24
|
+
Browser,
|
|
25
|
+
BrowserContext,
|
|
26
|
+
Page,
|
|
27
|
+
Playwright,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NVBrowserSession:
|
|
32
|
+
"""Lightweight wrapper around Playwright for non-visual (headless)
|
|
33
|
+
browsing.
|
|
34
|
+
|
|
35
|
+
It provides a single *Page* instance plus helper utilities (snapshot &
|
|
36
|
+
executor). Multiple toolkits or agents can reuse this class without
|
|
37
|
+
duplicating Playwright setup code.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Configuration constants
|
|
41
|
+
DEFAULT_NAVIGATION_TIMEOUT = 10000 # 10 seconds
|
|
42
|
+
NETWORK_IDLE_TIMEOUT = 5000 # 5 seconds
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self, *, headless: bool = True, user_data_dir: Optional[str] = None
|
|
46
|
+
):
|
|
47
|
+
self._headless = headless
|
|
48
|
+
self._user_data_dir = user_data_dir
|
|
49
|
+
|
|
50
|
+
self._playwright: Optional[Playwright] = None
|
|
51
|
+
self._browser: Optional[Browser] = None
|
|
52
|
+
self._context: Optional[BrowserContext] = None
|
|
53
|
+
self._page: Optional[Page] = None
|
|
54
|
+
|
|
55
|
+
self.snapshot: Optional[PageSnapshot] = None
|
|
56
|
+
self.executor: Optional[ActionExecutor] = None
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Browser lifecycle helpers
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
async def ensure_browser(self) -> None:
|
|
62
|
+
from playwright.async_api import async_playwright
|
|
63
|
+
|
|
64
|
+
if self._page is not None:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
self._playwright = await async_playwright().start()
|
|
68
|
+
if self._user_data_dir:
|
|
69
|
+
Path(self._user_data_dir).mkdir(parents=True, exist_ok=True)
|
|
70
|
+
pl = self._playwright
|
|
71
|
+
assert pl is not None
|
|
72
|
+
self._context = await pl.chromium.launch_persistent_context(
|
|
73
|
+
user_data_dir=self._user_data_dir,
|
|
74
|
+
headless=self._headless,
|
|
75
|
+
)
|
|
76
|
+
self._browser = self._context.browser
|
|
77
|
+
else:
|
|
78
|
+
pl = self._playwright
|
|
79
|
+
assert pl is not None
|
|
80
|
+
self._browser = await pl.chromium.launch(headless=self._headless)
|
|
81
|
+
self._context = await self._browser.new_context()
|
|
82
|
+
|
|
83
|
+
# Reuse an already open page (persistent context may restore last
|
|
84
|
+
# session)
|
|
85
|
+
if self._context.pages:
|
|
86
|
+
self._page = self._context.pages[0]
|
|
87
|
+
else:
|
|
88
|
+
self._page = await self._context.new_page()
|
|
89
|
+
# helpers
|
|
90
|
+
self.snapshot = PageSnapshot(self._page)
|
|
91
|
+
self.executor = ActionExecutor(self._page)
|
|
92
|
+
|
|
93
|
+
async def close(self) -> None:
|
|
94
|
+
r"""Close all browser resources, ensuring cleanup even if some
|
|
95
|
+
operations fail.
|
|
96
|
+
"""
|
|
97
|
+
errors: list[str] = []
|
|
98
|
+
|
|
99
|
+
# Close context first (which closes pages)
|
|
100
|
+
if self._context is not None:
|
|
101
|
+
try:
|
|
102
|
+
await self._context.close()
|
|
103
|
+
except Exception as e:
|
|
104
|
+
errors.append(f"Context close error: {e}")
|
|
105
|
+
|
|
106
|
+
# Close browser
|
|
107
|
+
if self._browser is not None:
|
|
108
|
+
try:
|
|
109
|
+
await self._browser.close()
|
|
110
|
+
except Exception as e:
|
|
111
|
+
errors.append(f"Browser close error: {e}")
|
|
112
|
+
|
|
113
|
+
# Stop playwright
|
|
114
|
+
if self._playwright is not None:
|
|
115
|
+
try:
|
|
116
|
+
await self._playwright.stop()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
errors.append(f"Playwright stop error: {e}")
|
|
119
|
+
|
|
120
|
+
# Reset all references
|
|
121
|
+
self._playwright = self._browser = self._context = self._page = None
|
|
122
|
+
self.snapshot = self.executor = None
|
|
123
|
+
|
|
124
|
+
# Log errors if any occurred during cleanup
|
|
125
|
+
if errors:
|
|
126
|
+
from camel.logger import get_logger
|
|
127
|
+
|
|
128
|
+
logger = get_logger(__name__)
|
|
129
|
+
logger.warning(
|
|
130
|
+
"Errors during browser session cleanup: %s", "; ".join(errors)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Convenience wrappers around common actions
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
async def visit(self, url: str) -> str:
|
|
137
|
+
await self.ensure_browser()
|
|
138
|
+
assert self._page is not None
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
await self._page.goto(
|
|
142
|
+
url,
|
|
143
|
+
wait_until="domcontentloaded",
|
|
144
|
+
timeout=self.DEFAULT_NAVIGATION_TIMEOUT,
|
|
145
|
+
)
|
|
146
|
+
# Try to wait for network idle, but don't fail if it times out
|
|
147
|
+
try:
|
|
148
|
+
await self._page.wait_for_load_state(
|
|
149
|
+
"networkidle", timeout=self.NETWORK_IDLE_TIMEOUT
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass # Network idle timeout is not critical
|
|
153
|
+
return f"Visited {url}"
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return f"Error visiting {url}: {e}"
|
|
156
|
+
|
|
157
|
+
async def get_snapshot(
|
|
158
|
+
self, *, force_refresh: bool = False, diff_only: bool = False
|
|
159
|
+
) -> str:
|
|
160
|
+
await self.ensure_browser()
|
|
161
|
+
assert self.snapshot is not None
|
|
162
|
+
return await self.snapshot.capture(
|
|
163
|
+
force_refresh=force_refresh, diff_only=diff_only
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
async def exec_action(self, action: dict[str, Any]) -> str:
|
|
167
|
+
await self.ensure_browser()
|
|
168
|
+
assert self.executor is not None
|
|
169
|
+
return await self.executor.execute(action)
|
|
170
|
+
|
|
171
|
+
# Low-level accessors -------------------------------------------------
|
|
172
|
+
async def get_page(self) -> "Page":
|
|
173
|
+
await self.ensure_browser()
|
|
174
|
+
assert self._page is not None
|
|
175
|
+
return self._page
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// Store each element as {text, priority, depth}
|
|
3
|
+
const elements = [];
|
|
4
|
+
|
|
5
|
+
// Maximum lines allowed before we start dropping lower-priority nodes
|
|
6
|
+
const MAX_LINES = 400;
|
|
7
|
+
|
|
8
|
+
// Priority helper – lower number = higher priority
|
|
9
|
+
function getPriority(tag, role, text) {
|
|
10
|
+
// 1. Interactive elements
|
|
11
|
+
if (["input", "button", "a", "select", "textarea"].includes(tag)) return 1;
|
|
12
|
+
if (["checkbox", "radio"].includes(role)) return 1;
|
|
13
|
+
|
|
14
|
+
// 2. Labels / descriptive adjacent text (label elements)
|
|
15
|
+
if (tag === "label") return 2;
|
|
16
|
+
|
|
17
|
+
// 3. General visible text
|
|
18
|
+
if (text) return 3;
|
|
19
|
+
|
|
20
|
+
// 4. Low-value structural nodes
|
|
21
|
+
return 4;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isVisible(node) {
|
|
25
|
+
const rect = node.getBoundingClientRect();
|
|
26
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
27
|
+
|
|
28
|
+
const style = window.getComputedStyle(node);
|
|
29
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getRole(node) {
|
|
35
|
+
const tag = node.tagName.toLowerCase();
|
|
36
|
+
const type = node.getAttribute('type');
|
|
37
|
+
|
|
38
|
+
if (node.getAttribute('role')) return node.getAttribute('role');
|
|
39
|
+
|
|
40
|
+
if (tag === 'input') {
|
|
41
|
+
if (type === 'checkbox') return 'checkbox';
|
|
42
|
+
if (type === 'radio') return 'radio';
|
|
43
|
+
return 'input';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (tag === 'button') return 'button';
|
|
47
|
+
if (tag === 'a') return 'link';
|
|
48
|
+
if (tag === 'select') return 'select';
|
|
49
|
+
if (tag === 'textarea') return 'textarea';
|
|
50
|
+
if (tag === 'p') return 'paragraph';
|
|
51
|
+
if (tag === 'span') return 'text';
|
|
52
|
+
|
|
53
|
+
return 'generic';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getAccessibleName(node) {
|
|
57
|
+
if (node.hasAttribute('aria-label')) {
|
|
58
|
+
return node.getAttribute('aria-label');
|
|
59
|
+
}
|
|
60
|
+
if (node.hasAttribute('aria-labelledby')) {
|
|
61
|
+
const id = node.getAttribute('aria-labelledby');
|
|
62
|
+
const labelEl = document.getElementById(id);
|
|
63
|
+
if (labelEl) return labelEl.textContent.trim();
|
|
64
|
+
}
|
|
65
|
+
if (node.hasAttribute('title')) {
|
|
66
|
+
return node.getAttribute('title');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tagName = node.tagName?.toLowerCase();
|
|
70
|
+
if (['style', 'script', 'meta', 'noscript', 'svg'].includes(tagName)) {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const text = node.textContent?.trim() || '';
|
|
75
|
+
|
|
76
|
+
// Ignore styles, tokens, or long CSS-like expressions
|
|
77
|
+
if (/^[.#]?[a-zA-Z0-9\-_]+\s*\{[^}]*\}/.test(text)) return '';
|
|
78
|
+
if ((text.match(/[;:{}]/g)?.length || 0) > 2) return '';
|
|
79
|
+
|
|
80
|
+
return text.replace(/[^\w\u4e00-\u9fa5\s\-.,?!'"()()]/g, '').trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let refCounter = 1;
|
|
84
|
+
|
|
85
|
+
function traverse(node, depth) {
|
|
86
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
87
|
+
if (!isVisible(node)) return;
|
|
88
|
+
|
|
89
|
+
const tagName = node.tagName.toLowerCase();
|
|
90
|
+
const text = getAccessibleName(node).slice(0, 50);
|
|
91
|
+
|
|
92
|
+
// Skip unlabeled links (anchors without any accessible name)
|
|
93
|
+
if (tagName === 'a' && !text) {
|
|
94
|
+
// Skip unlabeled links; process children if any
|
|
95
|
+
for (const child of node.children) {
|
|
96
|
+
traverse(child, depth + 1);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hasRoleOrText = ['button', 'a', 'input', 'select', 'textarea', 'p', 'span'].includes(tagName) ||
|
|
102
|
+
node.getAttribute('role') || text;
|
|
103
|
+
|
|
104
|
+
if (hasRoleOrText) {
|
|
105
|
+
const role = getRole(node);
|
|
106
|
+
const ref = `e${refCounter++}`;
|
|
107
|
+
const label = text ? `"${text}"` : '';
|
|
108
|
+
|
|
109
|
+
// Raw line (without indent) – we will apply indentation later once we know
|
|
110
|
+
// which ancestor lines survive filtering so that indentation always reflects
|
|
111
|
+
// the visible hierarchy.
|
|
112
|
+
const lineText = `- ${role}${label ? ` ${label}` : ''} [ref=${ref}]`;
|
|
113
|
+
const priority = getPriority(tagName, role, text);
|
|
114
|
+
|
|
115
|
+
elements.push({ text: lineText, priority, depth });
|
|
116
|
+
|
|
117
|
+
// Always inject ref so Playwright can still locate the element even if line is later filtered out.
|
|
118
|
+
node.setAttribute('aria-ref', ref);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const child of node.children) {
|
|
122
|
+
traverse(child, depth + 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function processDocument(doc, depth = 0) {
|
|
127
|
+
try {
|
|
128
|
+
traverse(doc.body, depth);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Handle docs without body (e.g., about:blank)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const frames = doc.querySelectorAll('iframe');
|
|
134
|
+
for (const frame of frames) {
|
|
135
|
+
try {
|
|
136
|
+
if (frame.contentDocument) {
|
|
137
|
+
processDocument(frame.contentDocument, depth + 1);
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// Skip cross-origin iframes
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
processDocument(document);
|
|
146
|
+
|
|
147
|
+
// Always drop priority-4 nodes (low-value structural or invisible)
|
|
148
|
+
let finalElements = elements.filter(el => el.priority <= 3);
|
|
149
|
+
|
|
150
|
+
// Additional size condensation when still exceeding MAX_LINES
|
|
151
|
+
if (finalElements.length > MAX_LINES) {
|
|
152
|
+
const filterBy = (maxPriority) => finalElements.filter(el => el.priority <= maxPriority);
|
|
153
|
+
|
|
154
|
+
// Progressively tighten: keep 1-3, then 1-2, finally only 1
|
|
155
|
+
for (const limit of [3, 2, 1]) {
|
|
156
|
+
const candidate = filterBy(limit);
|
|
157
|
+
if (candidate.length <= MAX_LINES || limit === 1) {
|
|
158
|
+
finalElements = candidate;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ------------------------------------------------------------------
|
|
165
|
+
// Re-apply indentation so that it matches the *visible* hierarchy only.
|
|
166
|
+
// Whenever an ancestor element is removed due to priority rules, its
|
|
167
|
+
// children will be re-indented one level up so the structure remains
|
|
168
|
+
// intuitive.
|
|
169
|
+
// ------------------------------------------------------------------
|
|
170
|
+
const outputLines = [];
|
|
171
|
+
const depthStack = []; // keeps track of kept original depths
|
|
172
|
+
|
|
173
|
+
for (const el of finalElements) {
|
|
174
|
+
// Pop depths that are not ancestors of current element
|
|
175
|
+
while (depthStack.length && depthStack[depthStack.length - 1] >= el.depth) {
|
|
176
|
+
depthStack.pop();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Push the current depth so future descendants know their ancestor chain
|
|
180
|
+
depthStack.push(el.depth);
|
|
181
|
+
|
|
182
|
+
const compressedDepth = depthStack.length - 1; // root level has zero indent
|
|
183
|
+
const indent = '\t'.repeat(compressedDepth);
|
|
184
|
+
outputLines.push(indent + el.text);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return outputLines.join('\n');
|
|
188
|
+
})();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from playwright.async_api import Page
|
|
19
|
+
|
|
20
|
+
# Logging support
|
|
21
|
+
from camel.logger import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PageSnapshot:
|
|
27
|
+
"""Utility for capturing YAML-like page snapshots and diff-only
|
|
28
|
+
variants."""
|
|
29
|
+
|
|
30
|
+
MAX_TIMEOUT_MS = 5000 # wait_for_load_state timeout
|
|
31
|
+
|
|
32
|
+
def __init__(self, page: "Page"):
|
|
33
|
+
self.page = page
|
|
34
|
+
self.snapshot_data: Optional[str] = None # last full snapshot
|
|
35
|
+
self._last_url: Optional[str] = None
|
|
36
|
+
self.last_info: Dict[str, List[int] | bool] = {
|
|
37
|
+
"is_diff": False,
|
|
38
|
+
"priorities": [1, 2, 3],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------
|
|
42
|
+
# Public API
|
|
43
|
+
# ---------------------------------------------------------------------
|
|
44
|
+
async def capture(
|
|
45
|
+
self, *, force_refresh: bool = False, diff_only: bool = False
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Return current snapshot or just the diff to previous one."""
|
|
48
|
+
try:
|
|
49
|
+
current_url = self.page.url
|
|
50
|
+
|
|
51
|
+
# Serve cached copy (unless diff requested)
|
|
52
|
+
if (
|
|
53
|
+
not force_refresh
|
|
54
|
+
and current_url == self._last_url
|
|
55
|
+
and self.snapshot_data
|
|
56
|
+
and not diff_only
|
|
57
|
+
):
|
|
58
|
+
return self.snapshot_data
|
|
59
|
+
|
|
60
|
+
# ensure DOM stability
|
|
61
|
+
await self.page.wait_for_load_state(
|
|
62
|
+
'domcontentloaded', timeout=self.MAX_TIMEOUT_MS
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.debug("Capturing page snapshot …")
|
|
66
|
+
snapshot_text = await self._get_snapshot_direct()
|
|
67
|
+
formatted = self._format_snapshot(snapshot_text or "<empty>")
|
|
68
|
+
|
|
69
|
+
output = formatted
|
|
70
|
+
if diff_only and self.snapshot_data:
|
|
71
|
+
output = self._compute_diff(self.snapshot_data, formatted)
|
|
72
|
+
|
|
73
|
+
# update cache with *full* snapshot (not diff)
|
|
74
|
+
self._last_url = current_url
|
|
75
|
+
self.snapshot_data = formatted
|
|
76
|
+
|
|
77
|
+
# analyse priorities present (only for non-diff)
|
|
78
|
+
priorities_included = self._detect_priorities(
|
|
79
|
+
formatted if not diff_only else self.snapshot_data or formatted
|
|
80
|
+
)
|
|
81
|
+
self.last_info = {
|
|
82
|
+
"is_diff": diff_only and self.snapshot_data is not None,
|
|
83
|
+
"priorities": priorities_included,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.debug(
|
|
87
|
+
"Snapshot captured. Diff_only=%s, priorities=%s",
|
|
88
|
+
diff_only,
|
|
89
|
+
self.last_info["priorities"],
|
|
90
|
+
)
|
|
91
|
+
return output
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
logger.error("Snapshot capture failed: %s", exc)
|
|
94
|
+
return f"Error: Could not capture page snapshot {exc}"
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Internal helpers
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
_snapshot_js_cache: Optional[str] = None # class-level cache
|
|
100
|
+
|
|
101
|
+
async def _get_snapshot_direct(self) -> Optional[str]:
|
|
102
|
+
try:
|
|
103
|
+
if PageSnapshot._snapshot_js_cache is None:
|
|
104
|
+
js_path = Path(__file__).parent / "snapshot.js"
|
|
105
|
+
PageSnapshot._snapshot_js_cache = js_path.read_text(
|
|
106
|
+
encoding="utf-8"
|
|
107
|
+
)
|
|
108
|
+
return await self.page.evaluate(PageSnapshot._snapshot_js_cache)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning("Failed to execute snapshot JavaScript: %s", e)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _format_snapshot(text: str) -> str:
|
|
115
|
+
return "\n".join(["- Page Snapshot", "```yaml", text, "```"])
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _compute_diff(old: str, new: str) -> str:
|
|
119
|
+
if not old or not new:
|
|
120
|
+
return "- Page Snapshot (error: missing data for diff)"
|
|
121
|
+
|
|
122
|
+
import difflib
|
|
123
|
+
|
|
124
|
+
diff = list(
|
|
125
|
+
difflib.unified_diff(
|
|
126
|
+
old.splitlines(False),
|
|
127
|
+
new.splitlines(False),
|
|
128
|
+
fromfile='prev',
|
|
129
|
+
tofile='curr',
|
|
130
|
+
lineterm='',
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
if not diff:
|
|
134
|
+
return "- Page Snapshot (no structural changes)"
|
|
135
|
+
return "\n".join(["- Page Snapshot (diff)", "```diff", *diff, "```"])
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
def _detect_priorities(self, snapshot_yaml: str) -> List[int]:
|
|
139
|
+
"""Return sorted list of priorities present (1,2,3)."""
|
|
140
|
+
priorities = set()
|
|
141
|
+
for line in snapshot_yaml.splitlines():
|
|
142
|
+
if '[ref=' not in line:
|
|
143
|
+
continue
|
|
144
|
+
lower_line = line.lower()
|
|
145
|
+
if any(
|
|
146
|
+
r in lower_line
|
|
147
|
+
for r in (
|
|
148
|
+
"input",
|
|
149
|
+
"button",
|
|
150
|
+
"select",
|
|
151
|
+
"textarea",
|
|
152
|
+
"checkbox",
|
|
153
|
+
"radio",
|
|
154
|
+
"link",
|
|
155
|
+
)
|
|
156
|
+
):
|
|
157
|
+
priorities.add(1)
|
|
158
|
+
elif 'label' in lower_line:
|
|
159
|
+
priorities.add(2)
|
|
160
|
+
else:
|
|
161
|
+
priorities.add(3)
|
|
162
|
+
if not priorities:
|
|
163
|
+
priorities.add(3)
|
|
164
|
+
return sorted(priorities)
|
camel/toolkits/pptx_toolkit.py
CHANGED
|
@@ -62,7 +62,7 @@ class PPTXToolkit(BaseToolkit):
|
|
|
62
62
|
output_dir (str): The default directory for output files.
|
|
63
63
|
Defaults to the current working directory.
|
|
64
64
|
timeout (Optional[float]): The timeout for the toolkit.
|
|
65
|
-
(default: :obj
|
|
65
|
+
(default: :obj:`None`)
|
|
66
66
|
"""
|
|
67
67
|
super().__init__(timeout=timeout)
|
|
68
68
|
self.output_dir = Path(output_dir).resolve()
|
|
@@ -120,7 +120,7 @@ class PPTXToolkit(BaseToolkit):
|
|
|
120
120
|
frame_paragraph: The paragraph to format.
|
|
121
121
|
text (str): The text to format.
|
|
122
122
|
set_color_to_white (bool): Whether to set the color to white.
|
|
123
|
-
(default: :obj
|
|
123
|
+
(default: :obj:`False`)
|
|
124
124
|
"""
|
|
125
125
|
from pptx.dml.color import RGBColor
|
|
126
126
|
|
|
@@ -170,7 +170,7 @@ class PPTXToolkit(BaseToolkit):
|
|
|
170
170
|
flat_items_list (List[Tuple[str, int]]): The list of items to be
|
|
171
171
|
displayed.
|
|
172
172
|
set_color_to_white (bool): Whether to set the font color to white.
|
|
173
|
-
(default: :obj
|
|
173
|
+
(default: :obj:`False`)
|
|
174
174
|
"""
|
|
175
175
|
if not flat_items_list:
|
|
176
176
|
logger.warning("Empty bullet point list provided")
|
|
@@ -368,7 +368,7 @@ class PPTXToolkit(BaseToolkit):
|
|
|
368
368
|
supplied, it is resolved to self.output_dir.
|
|
369
369
|
template (Optional[str]): The path to the template PPTX file.
|
|
370
370
|
Initializes a presentation from a given template file Or PPTX
|
|
371
|
-
file. (default: :obj
|
|
371
|
+
file. (default: :obj:`None`)
|
|
372
372
|
|
|
373
373
|
Returns:
|
|
374
374
|
str: A success message indicating the file was created.
|
camel/toolkits/sympy_toolkit.py
CHANGED
|
@@ -32,7 +32,7 @@ class TaskPlanningToolkit(BaseToolkit):
|
|
|
32
32
|
|
|
33
33
|
Args:
|
|
34
34
|
timeout (Optional[float]): The timeout for the toolkit.
|
|
35
|
-
(default: :obj
|
|
35
|
+
(default: :obj:`None`)
|
|
36
36
|
"""
|
|
37
37
|
super().__init__(timeout=timeout)
|
|
38
38
|
|
|
@@ -53,7 +53,7 @@ class TaskPlanningToolkit(BaseToolkit):
|
|
|
53
53
|
string is the content for a new sub-task.
|
|
54
54
|
original_task_id (Optional[str]): The id of the task to be
|
|
55
55
|
decomposed. If not provided, a new id will be generated.
|
|
56
|
-
(default: :obj
|
|
56
|
+
(default: :obj:`None`)
|
|
57
57
|
|
|
58
58
|
Returns:
|
|
59
59
|
List[Task]: A list of newly created sub-task objects.
|
|
@@ -99,7 +99,7 @@ class TaskPlanningToolkit(BaseToolkit):
|
|
|
99
99
|
sub_task_contents (List[str]): A list of strings, where each
|
|
100
100
|
string is the content for a new sub-task.
|
|
101
101
|
original_task_id (Optional[str]): The id of the task to be
|
|
102
|
-
decomposed. (default: :obj
|
|
102
|
+
decomposed. (default: :obj:`None`)
|
|
103
103
|
|
|
104
104
|
Returns:
|
|
105
105
|
List[Task]: Reordered or modified tasks.
|