alpiecode 10.5.5__tar.gz → 10.5.7__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.
- {alpiecode-10.5.5 → alpiecode-10.5.7}/PKG-INFO +1 -4
- {alpiecode-10.5.5 → alpiecode-10.5.7}/pyproject.toml +1 -7
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode.egg-info/PKG-INFO +1 -4
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode.egg-info/requires.txt +0 -4
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/backends/openai_backend.py +135 -11
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/tools.py +35 -55
- {alpiecode-10.5.5 → alpiecode-10.5.7}/README.md +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/setup.cfg +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/agent.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/assessment.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/cache.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/cli.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/client.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/config.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/context.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/dream_rsi.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/executor.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/github.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/local_model.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/media.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/memory.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/metrics.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/orchestrator.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/progress.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/prompt.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/server.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/session.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/system1.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/updater.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/verifier.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/test/test_adaptive_resilience.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/test/test_cli_ui.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/test/test_context_and_trajectory.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/test/test_dream_rsi.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/test/test_task_assessment.py +0 -0
- {alpiecode-10.5.5 → alpiecode-10.5.7}/test/test_verifier.py +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 10.5.
|
|
3
|
+
Version: 10.5.7
|
|
4
4
|
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
7
7
|
Requires-Dist: rich>=13.0
|
|
8
8
|
Requires-Dist: click>=8.0
|
|
9
|
-
Requires-Dist: openai>=1.0.0
|
|
10
9
|
Requires-Dist: httpx[http2]>=0.24.0
|
|
11
10
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
12
11
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
@@ -14,5 +13,3 @@ Requires-Dist: imageio-ffmpeg>=0.4.0
|
|
|
14
13
|
Requires-Dist: fastapi>=0.100.0
|
|
15
14
|
Requires-Dist: uvicorn>=0.20.0
|
|
16
15
|
Requires-Dist: sse-starlette>=2.0.0
|
|
17
|
-
Provides-Extra: search
|
|
18
|
-
Requires-Dist: duckduckgo-search>=7.0.0; extra == "search"
|
|
@@ -4,14 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpiecode"
|
|
7
|
-
version = "10.5.
|
|
7
|
+
version = "10.5.7"
|
|
8
8
|
description = "AlpieCode"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"huggingface_hub>=0.20.0",
|
|
12
12
|
"rich>=13.0",
|
|
13
13
|
"click>=8.0",
|
|
14
|
-
"openai>=1.0.0",
|
|
15
14
|
"httpx[http2]>=0.24.0",
|
|
16
15
|
"beautifulsoup4>=4.12.0",
|
|
17
16
|
"yt-dlp>=2024.0.0",
|
|
@@ -21,11 +20,6 @@ dependencies = [
|
|
|
21
20
|
"sse-starlette>=2.0.0",
|
|
22
21
|
]
|
|
23
22
|
|
|
24
|
-
[project.optional-dependencies]
|
|
25
|
-
search = [
|
|
26
|
-
"duckduckgo-search>=7.0.0",
|
|
27
|
-
]
|
|
28
|
-
|
|
29
23
|
[project.scripts]
|
|
30
24
|
alpiecode = "codeagent.cli:main"
|
|
31
25
|
codeagent = "codeagent.cli:main"
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 10.5.
|
|
3
|
+
Version: 10.5.7
|
|
4
4
|
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
7
7
|
Requires-Dist: rich>=13.0
|
|
8
8
|
Requires-Dist: click>=8.0
|
|
9
|
-
Requires-Dist: openai>=1.0.0
|
|
10
9
|
Requires-Dist: httpx[http2]>=0.24.0
|
|
11
10
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
12
11
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
@@ -14,5 +13,3 @@ Requires-Dist: imageio-ffmpeg>=0.4.0
|
|
|
14
13
|
Requires-Dist: fastapi>=0.100.0
|
|
15
14
|
Requires-Dist: uvicorn>=0.20.0
|
|
16
15
|
Requires-Dist: sse-starlette>=2.0.0
|
|
17
|
-
Provides-Extra: search
|
|
18
|
-
Requires-Dist: duckduckgo-search>=7.0.0; extra == "search"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
huggingface_hub>=0.20.0
|
|
2
2
|
rich>=13.0
|
|
3
3
|
click>=8.0
|
|
4
|
-
openai>=1.0.0
|
|
5
4
|
httpx[http2]>=0.24.0
|
|
6
5
|
beautifulsoup4>=4.12.0
|
|
7
6
|
yt-dlp>=2024.0.0
|
|
@@ -9,6 +8,3 @@ imageio-ffmpeg>=0.4.0
|
|
|
9
8
|
fastapi>=0.100.0
|
|
10
9
|
uvicorn>=0.20.0
|
|
11
10
|
sse-starlette>=2.0.0
|
|
12
|
-
|
|
13
|
-
[search]
|
|
14
|
-
duckduckgo-search>=7.0.0
|
|
@@ -17,7 +17,7 @@ Supports:
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any, Dict
|
|
19
19
|
|
|
20
|
-
__version__ = "10.5.
|
|
20
|
+
__version__ = "10.5.7"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -7,11 +7,138 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import re
|
|
9
9
|
from typing import Any, List, Optional
|
|
10
|
-
from
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from typing import Any, Dict, Generator, List, Optional
|
|
12
|
+
import httpx
|
|
11
13
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
class NotFoundError(Exception):
|
|
18
|
+
"""Raised when remote endpoint returns 404 Not Found."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APIError(Exception):
|
|
23
|
+
"""Raised when remote endpoint returns non-200 HTTP status."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Namespace(SimpleNamespace):
|
|
28
|
+
"""SimpleNamespace subclass that safely returns None for missing attributes instead of raising AttributeError."""
|
|
29
|
+
def __getattr__(self, name: str) -> Any:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _dict_to_obj(d: Any) -> Any:
|
|
34
|
+
"""Recursively wraps a dict into a _Namespace so attribute access works seamlessly."""
|
|
35
|
+
if isinstance(d, dict):
|
|
36
|
+
return _Namespace(**{k: _dict_to_obj(v) for k, v in d.items()})
|
|
37
|
+
if isinstance(d, list):
|
|
38
|
+
return [_dict_to_obj(item) for item in d]
|
|
39
|
+
return d
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _HTTPXChatCompletions:
|
|
43
|
+
def __init__(self, client: "_HTTPXClient"):
|
|
44
|
+
self._client = client
|
|
45
|
+
|
|
46
|
+
def create(self, **kwargs: Any) -> Any:
|
|
47
|
+
stream = kwargs.get("stream", False)
|
|
48
|
+
timeout = kwargs.get("timeout", 90.0)
|
|
49
|
+
|
|
50
|
+
payload: Dict[str, Any] = {
|
|
51
|
+
"model": kwargs.get("model"),
|
|
52
|
+
"messages": kwargs.get("messages", []),
|
|
53
|
+
"temperature": kwargs.get("temperature", 0.1),
|
|
54
|
+
"max_tokens": kwargs.get("max_tokens", 8192),
|
|
55
|
+
"stream": stream,
|
|
56
|
+
}
|
|
57
|
+
if "tools" in kwargs and kwargs["tools"]:
|
|
58
|
+
payload["tools"] = kwargs["tools"]
|
|
59
|
+
payload["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
60
|
+
if "extra_body" in kwargs and isinstance(kwargs["extra_body"], dict):
|
|
61
|
+
payload.update(kwargs["extra_body"])
|
|
62
|
+
|
|
63
|
+
headers = {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Authorization": f"Bearer {self._client.api_key}",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
base = self._client.base_url.rstrip("/")
|
|
69
|
+
url = f"{base}/chat/completions"
|
|
70
|
+
|
|
71
|
+
if stream:
|
|
72
|
+
return self._stream_request(url, headers, payload, timeout)
|
|
73
|
+
return self._sync_request(url, headers, payload, timeout)
|
|
74
|
+
|
|
75
|
+
def _stream_request(self, url: str, headers: dict, payload: dict, timeout: float) -> Generator[Any, None, None]:
|
|
76
|
+
http_client = self._client.http_client
|
|
77
|
+
req = http_client.build_request("POST", url, headers=headers, json=payload, timeout=timeout)
|
|
78
|
+
resp = http_client.send(req, stream=True)
|
|
79
|
+
if resp.status_code == 404:
|
|
80
|
+
resp.close()
|
|
81
|
+
raise NotFoundError("Model or endpoint not found")
|
|
82
|
+
if resp.status_code != 200:
|
|
83
|
+
err_text = resp.read().decode("utf-8", errors="replace")
|
|
84
|
+
resp.close()
|
|
85
|
+
raise APIError(f"HTTP {resp.status_code}: {err_text}")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
for line in resp.iter_lines():
|
|
89
|
+
line = line.strip()
|
|
90
|
+
if not line or not line.startswith("data: "):
|
|
91
|
+
continue
|
|
92
|
+
data_str = line[6:].strip()
|
|
93
|
+
if data_str == "[DONE]":
|
|
94
|
+
break
|
|
95
|
+
try:
|
|
96
|
+
chunk_dict = json.loads(data_str)
|
|
97
|
+
except Exception:
|
|
98
|
+
continue
|
|
99
|
+
yield _dict_to_obj(chunk_dict)
|
|
100
|
+
finally:
|
|
101
|
+
resp.close()
|
|
102
|
+
|
|
103
|
+
def _sync_request(self, url: str, headers: dict, payload: dict, timeout: float) -> Any:
|
|
104
|
+
resp = self._client.http_client.post(url, headers=headers, json=payload, timeout=timeout)
|
|
105
|
+
if resp.status_code == 404:
|
|
106
|
+
raise NotFoundError("Model or endpoint not found")
|
|
107
|
+
if resp.status_code != 200:
|
|
108
|
+
raise APIError(f"HTTP {resp.status_code}: {resp.text}")
|
|
109
|
+
data = json.loads(resp.text)
|
|
110
|
+
return _dict_to_obj(data)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _HTTPXModels:
|
|
114
|
+
def __init__(self, client: "_HTTPXClient"):
|
|
115
|
+
self._client = client
|
|
116
|
+
|
|
117
|
+
def list(self) -> Any:
|
|
118
|
+
base = self._client.base_url.rstrip("/")
|
|
119
|
+
url = f"{base}/models"
|
|
120
|
+
headers = {
|
|
121
|
+
"Authorization": f"Bearer {self._client.api_key}",
|
|
122
|
+
}
|
|
123
|
+
try:
|
|
124
|
+
resp = self._client.http_client.get(url, headers=headers, timeout=3.0)
|
|
125
|
+
if resp.status_code != 200:
|
|
126
|
+
return _Namespace(data=[])
|
|
127
|
+
data = json.loads(resp.text)
|
|
128
|
+
return _dict_to_obj(data)
|
|
129
|
+
except Exception:
|
|
130
|
+
return _Namespace(data=[])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _HTTPXClient:
|
|
134
|
+
def __init__(self, base_url: str, api_key: str, http_client: Any = None):
|
|
135
|
+
self.base_url = base_url
|
|
136
|
+
self.api_key = api_key or "not-needed"
|
|
137
|
+
self.http_client = http_client if http_client is not None else get_shared_http_client()
|
|
138
|
+
self.chat = _Namespace(completions=_HTTPXChatCompletions(self))
|
|
139
|
+
self.models = _HTTPXModels(self)
|
|
140
|
+
|
|
141
|
+
|
|
15
142
|
def _repair_and_parse_json_arguments(raw_args: str) -> dict:
|
|
16
143
|
"""
|
|
17
144
|
Robustly parse tool call arguments from potentially partial or malformed streaming strings.
|
|
@@ -125,7 +252,7 @@ class OpenAIBackend:
|
|
|
125
252
|
|
|
126
253
|
def __init__(self, cfg: Config):
|
|
127
254
|
self._cfg = cfg
|
|
128
|
-
self._client: Optional[
|
|
255
|
+
self._client: Optional[Any] = None
|
|
129
256
|
self._resolved_model: Optional[str] = None
|
|
130
257
|
self._max_model_len: Optional[int] = None
|
|
131
258
|
|
|
@@ -143,16 +270,16 @@ class OpenAIBackend:
|
|
|
143
270
|
return self._max_model_len
|
|
144
271
|
return 262_144
|
|
145
272
|
|
|
146
|
-
def _ensure_client(self) ->
|
|
273
|
+
def _ensure_client(self) -> Any:
|
|
147
274
|
if self._client is None:
|
|
148
|
-
self._client =
|
|
275
|
+
self._client = _HTTPXClient(
|
|
149
276
|
base_url=self._cfg.base_url,
|
|
150
277
|
api_key=self._cfg.api_key or "not-needed",
|
|
151
278
|
http_client=get_shared_http_client(),
|
|
152
279
|
)
|
|
153
280
|
return self._client
|
|
154
281
|
|
|
155
|
-
def _get_available_model(self, client:
|
|
282
|
+
def _get_available_model(self, client: Any) -> str:
|
|
156
283
|
"""Auto-discover the served model name from /v1/models.
|
|
157
284
|
|
|
158
285
|
Uses a dedicated short-timeout (3s) probe to avoid blocking the agent
|
|
@@ -163,18 +290,15 @@ class OpenAIBackend:
|
|
|
163
290
|
return self._resolved_model
|
|
164
291
|
try:
|
|
165
292
|
import httpx
|
|
166
|
-
|
|
167
|
-
# The shared HTTP client has a 120s read timeout which causes
|
|
168
|
-
# multi-minute hangs when /v1/models is slow or unresponsive.
|
|
169
|
-
probe_client = OpenAI(
|
|
293
|
+
probe_client = _HTTPXClient(
|
|
170
294
|
base_url=self._cfg.base_url,
|
|
171
295
|
api_key=self._cfg.api_key or "not-needed",
|
|
172
296
|
http_client=httpx.Client(timeout=httpx.Timeout(3.0, connect=2.0)),
|
|
173
297
|
)
|
|
174
298
|
models_list = probe_client.models.list()
|
|
175
|
-
if models_list.data:
|
|
299
|
+
if hasattr(models_list, "data") and models_list.data:
|
|
176
300
|
for m in models_list.data:
|
|
177
|
-
if m
|
|
301
|
+
if getattr(m, "id", None) == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
|
|
178
302
|
self._resolved_model = m.id
|
|
179
303
|
self._max_model_len = getattr(m, "max_model_len", None)
|
|
180
304
|
logger.info("Resolved model: %s (max_len=%s)", self._resolved_model, self._max_model_len)
|
|
@@ -760,14 +760,15 @@ def _apply_patch(workdir: Path, path: str, patch: str) -> str:
|
|
|
760
760
|
|
|
761
761
|
|
|
762
762
|
def _web_search(query: str, num_results: int = 5) -> str:
|
|
763
|
-
"""Search the web using httpx
|
|
763
|
+
"""Search the web using httpx and DuckDuckGo HTTP endpoints, parsed via BeautifulSoup."""
|
|
764
764
|
import warnings
|
|
765
765
|
import urllib.parse
|
|
766
766
|
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
|
767
767
|
|
|
768
|
-
# 1. Primary: Pure-Python search using httpx (no Rust/primp native dependencies)
|
|
769
768
|
try:
|
|
770
769
|
import httpx
|
|
770
|
+
from bs4 import BeautifulSoup
|
|
771
|
+
|
|
771
772
|
headers = {
|
|
772
773
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
773
774
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
@@ -779,65 +780,44 @@ def _web_search(query: str, num_results: int = 5) -> str:
|
|
|
779
780
|
resp = client.post("https://lite.duckduckgo.com/lite/", data={"q": query}, headers=headers)
|
|
780
781
|
|
|
781
782
|
if resp.status_code == 200 and resp.text:
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
783
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
784
|
+
results = []
|
|
785
|
+
for item in soup.select(".result"):
|
|
786
|
+
title_elem = item.select_one(".result__title a")
|
|
787
|
+
snippet_elem = item.select_one(".result__snippet")
|
|
788
|
+
if title_elem:
|
|
789
|
+
title = title_elem.get_text(strip=True)
|
|
790
|
+
href = title_elem.get("href", "")
|
|
791
|
+
if "uddg=" in href:
|
|
792
|
+
parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
|
|
793
|
+
href = parsed.get("uddg", [href])[0]
|
|
794
|
+
snippet = snippet_elem.get_text(strip=True) if snippet_elem else ""
|
|
795
|
+
results.append({"title": title, "href": href, "body": snippet})
|
|
796
|
+
if len(results) >= min(num_results, 10):
|
|
797
|
+
break
|
|
798
|
+
|
|
799
|
+
if not results:
|
|
800
|
+
for row in soup.select("table tr"):
|
|
801
|
+
link = row.select_one("a.result-link")
|
|
802
|
+
snippet = row.select_one(".result-snippet")
|
|
803
|
+
if link:
|
|
804
|
+
title = link.get_text(strip=True)
|
|
805
|
+
href = link.get("href", "")
|
|
792
806
|
if "uddg=" in href:
|
|
793
807
|
parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
|
|
794
808
|
href = parsed.get("uddg", [href])[0]
|
|
795
|
-
|
|
796
|
-
results.append({"title": title, "href": href, "body":
|
|
809
|
+
snip_text = snippet.get_text(strip=True) if snippet else ""
|
|
810
|
+
results.append({"title": title, "href": href, "body": snip_text})
|
|
797
811
|
if len(results) >= min(num_results, 10):
|
|
798
812
|
break
|
|
799
813
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
if "uddg=" in href:
|
|
808
|
-
parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
|
|
809
|
-
href = parsed.get("uddg", [href])[0]
|
|
810
|
-
snip_text = snippet.get_text(strip=True) if snippet else ""
|
|
811
|
-
results.append({"title": title, "href": href, "body": snip_text})
|
|
812
|
-
if len(results) >= min(num_results, 10):
|
|
813
|
-
break
|
|
814
|
-
|
|
815
|
-
if results:
|
|
816
|
-
formatted = []
|
|
817
|
-
for i, r in enumerate(results, 1):
|
|
818
|
-
formatted.append(f"{i}. **{r.get('title', 'No title')}**\n URL: {r.get('href', '')}\n {r.get('body', '')}")
|
|
819
|
-
return "\n\n".join(formatted)
|
|
820
|
-
except Exception:
|
|
821
|
-
pass
|
|
822
|
-
except Exception:
|
|
823
|
-
pass
|
|
824
|
-
|
|
825
|
-
# 2. Secondary fallback: duckduckgo_search if optionally installed
|
|
826
|
-
try:
|
|
827
|
-
try:
|
|
828
|
-
from ddgs import DDGS
|
|
829
|
-
except ImportError:
|
|
830
|
-
from duckduckgo_search import DDGS
|
|
831
|
-
|
|
832
|
-
with DDGS() as ddgs:
|
|
833
|
-
results = list(ddgs.text(query, max_results=min(num_results, 10)))
|
|
834
|
-
if results:
|
|
835
|
-
formatted = []
|
|
836
|
-
for i, r in enumerate(results, 1):
|
|
837
|
-
formatted.append(f"{i}. **{r.get('title', 'No title')}**\n URL: {r.get('href', '')}\n {r.get('body', '')}")
|
|
838
|
-
return "\n\n".join(formatted)
|
|
839
|
-
except Exception:
|
|
840
|
-
pass
|
|
814
|
+
if results:
|
|
815
|
+
formatted = []
|
|
816
|
+
for i, r in enumerate(results, 1):
|
|
817
|
+
formatted.append(f"{i}. **{r.get('title', 'No title')}**\n URL: {r.get('href', '')}\n {r.get('body', '')}")
|
|
818
|
+
return "\n\n".join(formatted)
|
|
819
|
+
except Exception as e:
|
|
820
|
+
return f"error during web search: {e}"
|
|
841
821
|
|
|
842
822
|
return (
|
|
843
823
|
f"No web results found for '{query}'. "
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|