@agxnte/reidx 2.0.2
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.
- package/README.md +724 -0
- package/bin/reidx.js +33 -0
- package/package.json +40 -0
- package/pyproject.toml +45 -0
- package/scripts/find-python.js +24 -0
- package/scripts/postinstall.js +35 -0
- package/src/reidx/__init__.py +3 -0
- package/src/reidx/__main__.py +9 -0
- package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__init__.py +3 -0
- package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/app/commands.py +221 -0
- package/src/reidx/automation/__init__.py +3 -0
- package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
- package/src/reidx/automation/exec.py +35 -0
- package/src/reidx/config/__init__.py +10 -0
- package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
- package/src/reidx/config/loader.py +104 -0
- package/src/reidx/config/models.py +49 -0
- package/src/reidx/config/settings.py +127 -0
- package/src/reidx/deepreid/__init__.py +8 -0
- package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/pipeline.py +221 -0
- package/src/reidx/diagnostics/__init__.py +0 -0
- package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/logger.py +56 -0
- package/src/reidx/goals/__init__.py +21 -0
- package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/goals/models.py +101 -0
- package/src/reidx/goals/store.py +233 -0
- package/src/reidx/integrations/__init__.py +3 -0
- package/src/reidx/integrations/mcp.py +70 -0
- package/src/reidx/nyx/__init__.py +3 -0
- package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
- package/src/reidx/nyx/persona.py +35 -0
- package/src/reidx/policy/__init__.py +15 -0
- package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/policy/engine.py +101 -0
- package/src/reidx/policy/models.py +35 -0
- package/src/reidx/provider/__init__.py +16 -0
- package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
- package/src/reidx/provider/_http.py +29 -0
- package/src/reidx/provider/anthropic.py +176 -0
- package/src/reidx/provider/base.py +50 -0
- package/src/reidx/provider/ollama.py +109 -0
- package/src/reidx/provider/openai.py +144 -0
- package/src/reidx/provider/registry.py +88 -0
- package/src/reidx/provider/store.py +141 -0
- package/src/reidx/provider/stub.py +60 -0
- package/src/reidx/provider_manager/__init__.py +40 -0
- package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/catalog.py +330 -0
- package/src/reidx/provider_manager/database.py +234 -0
- package/src/reidx/provider_manager/keychain.py +102 -0
- package/src/reidx/provider_manager/palette.py +831 -0
- package/src/reidx/runtime/__init__.py +5 -0
- package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/agent.py +198 -0
- package/src/reidx/runtime/orchestrator.py +244 -0
- package/src/reidx/runtime/reasoning.py +77 -0
- package/src/reidx/runtime/state.py +32 -0
- package/src/reidx/runtime/subagent.py +174 -0
- package/src/reidx/session/__init__.py +4 -0
- package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/session/models.py +43 -0
- package/src/reidx/session/store.py +101 -0
- package/src/reidx/tasks/__init__.py +4 -0
- package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/tasks/models.py +44 -0
- package/src/reidx/tasks/store.py +86 -0
- package/src/reidx/tools/__init__.py +44 -0
- package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/base.py +84 -0
- package/src/reidx/tools/file_tools.py +222 -0
- package/src/reidx/tools/registry.py +45 -0
- package/src/reidx/tools/shell_tool.py +75 -0
- package/src/reidx/tools/spawn_agent.py +185 -0
- package/src/reidx/tools/web_tools.py +216 -0
- package/src/reidx/ui/__init__.py +41 -0
- package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
- package/src/reidx/ui/app.py +1343 -0
- package/src/reidx/ui/commands.py +663 -0
- package/src/reidx/ui/render.py +517 -0
- package/src/reidx/ui/repl.py +14 -0
- package/src/reidx/ui/theme.py +126 -0
- package/src/reidx/workflows/__init__.py +4 -0
- package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/workflows/models.py +19 -0
- package/src/reidx/workflows/store.py +56 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Web search tool: free, no API key.
|
|
2
|
+
|
|
3
|
+
Two DuckDuckGo sources, tried in order:
|
|
4
|
+
|
|
5
|
+
1. The official Instant Answer JSON API (api.duckduckgo.com) — a separate,
|
|
6
|
+
lightly-protected endpoint meant for programmatic use. ~10-20x faster than
|
|
7
|
+
the HTML scrape (small JSON payload, no page rendering markup) but only
|
|
8
|
+
populated for factual/entity queries ("what is X", "who is Y") — general
|
|
9
|
+
searches come back empty.
|
|
10
|
+
2. The HTML-only endpoint (html.duckduckgo.com/html/) — the same page
|
|
11
|
+
DuckDuckGo serves to browsers with JavaScript disabled. Slower and more
|
|
12
|
+
prone to anti-bot rate limiting, but covers general search queries the
|
|
13
|
+
Instant Answer API doesn't.
|
|
14
|
+
|
|
15
|
+
Falls through to (2) only when (1) has nothing useful, so most factual
|
|
16
|
+
lookups get the fast path. Gzip-compressed responses (smaller/faster
|
|
17
|
+
transfer) and a short-lived in-memory cache (repeat queries within a session
|
|
18
|
+
skip the network entirely) are used for both. Stdlib-only (urllib + re) to
|
|
19
|
+
avoid new dependencies. Gated as ActionKind.NETWORK (HIGH risk by default)
|
|
20
|
+
through the same policy engine as every other tool.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import gzip
|
|
25
|
+
import html
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import time
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.parse
|
|
31
|
+
import urllib.request
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from reidx.policy.models import ActionKind, PermissionDecision, RiskLevel
|
|
35
|
+
from reidx.tools.base import BaseTool, ToolContext, ToolDefinition, ToolResult
|
|
36
|
+
|
|
37
|
+
_INSTANT_ANSWER_URL = "https://api.duckduckgo.com/"
|
|
38
|
+
_HTML_SEARCH_URL = "https://html.duckduckgo.com/html/"
|
|
39
|
+
_USER_AGENT = (
|
|
40
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
41
|
+
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
42
|
+
)
|
|
43
|
+
_HEADERS = {"User-Agent": _USER_AGENT, "Accept-Encoding": "gzip"}
|
|
44
|
+
_TIMEOUT_SECONDS = 10
|
|
45
|
+
_CACHE_TTL_SECONDS = 300
|
|
46
|
+
|
|
47
|
+
# DuckDuckGo's HTML results: <a class="result__a" href="...">title</a> ... <a class="result__snippet" ...>snippet</a>
|
|
48
|
+
_RESULT_RE = re.compile(
|
|
49
|
+
r'class="result__a"[^>]*href="(?P<href>[^"]+)"[^>]*>(?P<title>.*?)</a>'
|
|
50
|
+
r'.*?class="result__snippet"[^>]*>(?P<snippet>.*?)</a>',
|
|
51
|
+
re.DOTALL,
|
|
52
|
+
)
|
|
53
|
+
_TAG_RE = re.compile(r"<[^>]+>")
|
|
54
|
+
|
|
55
|
+
# DuckDuckGo serves this instead of results when it suspects automation
|
|
56
|
+
# (common from datacenter/shared IPs, or after rapid repeated queries).
|
|
57
|
+
_BOT_CHALLENGE_MARKER = "bots use duckduckgo too"
|
|
58
|
+
|
|
59
|
+
_PARAMS = {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"query": {"type": "string", "description": "Search query."},
|
|
63
|
+
"max_results": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"description": "Maximum results to return (default 5, max 10).",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"required": ["query"],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _clean(html_fragment: str) -> str:
|
|
73
|
+
text = _TAG_RE.sub("", html_fragment)
|
|
74
|
+
text = html.unescape(text)
|
|
75
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_href(href: str) -> str | None:
|
|
79
|
+
"""DuckDuckGo wraps organic result links in a `/l/?uddg=...` redirect;
|
|
80
|
+
unwrap to the real URL. Sponsored results redirect through a different,
|
|
81
|
+
non-unwrappable tracking endpoint (e.g. `/y.js`) — treat those as
|
|
82
|
+
unresolvable and drop them rather than surface a raw ad-click URL."""
|
|
83
|
+
if href.startswith("//"):
|
|
84
|
+
href = "https:" + href
|
|
85
|
+
parsed = urllib.parse.urlparse(href)
|
|
86
|
+
if parsed.netloc.endswith("duckduckgo.com"):
|
|
87
|
+
if parsed.path == "/l/":
|
|
88
|
+
real = urllib.parse.parse_qs(parsed.query).get("uddg")
|
|
89
|
+
if real:
|
|
90
|
+
return urllib.parse.unquote(real[0])
|
|
91
|
+
return None
|
|
92
|
+
return href
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _fetch(url: str) -> bytes:
|
|
96
|
+
request = urllib.request.Request(url, headers=_HEADERS)
|
|
97
|
+
with urllib.request.urlopen(request, timeout=_TIMEOUT_SECONDS) as resp: # noqa: S310
|
|
98
|
+
raw = resp.read()
|
|
99
|
+
if resp.headers.get("Content-Encoding") == "gzip":
|
|
100
|
+
raw = gzip.decompress(raw)
|
|
101
|
+
return raw
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _instant_answer(query: str, max_results: int) -> list[dict[str, str]]:
|
|
105
|
+
"""Fast path: DuckDuckGo's official JSON API. Empty for general searches."""
|
|
106
|
+
url = f"{_INSTANT_ANSWER_URL}?{urllib.parse.urlencode({'q': query, 'format': 'json', 'no_html': '1', 'skip_disambig': '1'})}"
|
|
107
|
+
data = json.loads(_fetch(url))
|
|
108
|
+
|
|
109
|
+
results: list[dict[str, str]] = []
|
|
110
|
+
abstract = (data.get("AbstractText") or "").strip()
|
|
111
|
+
if abstract:
|
|
112
|
+
results.append(
|
|
113
|
+
{
|
|
114
|
+
"title": data.get("Heading") or query,
|
|
115
|
+
"url": data.get("AbstractURL") or "",
|
|
116
|
+
"snippet": abstract,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
for topic in data.get("RelatedTopics", []):
|
|
120
|
+
if len(results) >= max_results:
|
|
121
|
+
break
|
|
122
|
+
if not isinstance(topic, dict) or not topic.get("Text"):
|
|
123
|
+
continue
|
|
124
|
+
text = topic["Text"]
|
|
125
|
+
title = text.split(" - ", 1)[0]
|
|
126
|
+
results.append({"title": title, "url": topic.get("FirstURL") or "", "snippet": text})
|
|
127
|
+
return results[:max_results]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _html_search(query: str, max_results: int) -> list[dict[str, str]]:
|
|
131
|
+
"""Fallback: scrape the HTML-only search endpoint for general queries."""
|
|
132
|
+
url = f"{_HTML_SEARCH_URL}?{urllib.parse.urlencode({'q': query})}"
|
|
133
|
+
page_html = _fetch(url).decode("utf-8", errors="replace")
|
|
134
|
+
|
|
135
|
+
if _BOT_CHALLENGE_MARKER in page_html.lower():
|
|
136
|
+
raise _BotChallenge
|
|
137
|
+
|
|
138
|
+
results: list[dict[str, str]] = []
|
|
139
|
+
for match in _RESULT_RE.finditer(page_html):
|
|
140
|
+
title = _clean(match.group("title"))
|
|
141
|
+
snippet = _clean(match.group("snippet"))
|
|
142
|
+
href = _resolve_href(match.group("href"))
|
|
143
|
+
if not title or not href:
|
|
144
|
+
continue
|
|
145
|
+
results.append({"title": title, "url": href, "snippet": snippet})
|
|
146
|
+
if len(results) >= max_results:
|
|
147
|
+
break
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class _BotChallenge(Exception):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class WebSearchTool(BaseTool):
|
|
156
|
+
def __init__(self) -> None:
|
|
157
|
+
self._cache: dict[tuple[str, int], tuple[float, list[dict[str, str]]]] = {}
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def definition(self) -> ToolDefinition:
|
|
161
|
+
return ToolDefinition(
|
|
162
|
+
name="web_search",
|
|
163
|
+
description=(
|
|
164
|
+
"Search the web via DuckDuckGo (free, no API key). Returns the top "
|
|
165
|
+
"result titles, URLs, and snippets for a query."
|
|
166
|
+
),
|
|
167
|
+
parameters=_PARAMS,
|
|
168
|
+
risk=RiskLevel.HIGH, # matches PolicyEngine's ActionKind.NETWORK default
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def execute(self, args: dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
172
|
+
query = str(args.get("query", "")).strip()
|
|
173
|
+
if not query:
|
|
174
|
+
return ToolResult.fail("query is required")
|
|
175
|
+
max_results = args.get("max_results") or 5
|
|
176
|
+
try:
|
|
177
|
+
max_results = max(1, min(int(max_results), 10))
|
|
178
|
+
except (TypeError, ValueError):
|
|
179
|
+
max_results = 5
|
|
180
|
+
|
|
181
|
+
cache_key = (query.lower(), max_results)
|
|
182
|
+
cached = self._cache.get(cache_key)
|
|
183
|
+
if cached is not None and time.monotonic() - cached[0] < _CACHE_TTL_SECONDS:
|
|
184
|
+
return self._format(query, cached[1])
|
|
185
|
+
|
|
186
|
+
decision = ctx.policy.evaluate(ActionKind.NETWORK)
|
|
187
|
+
if decision is PermissionDecision.DENY:
|
|
188
|
+
return ToolResult.fail("web search blocked by policy")
|
|
189
|
+
if decision is PermissionDecision.PROMPT:
|
|
190
|
+
if ctx.resolve_decision(f'Search the web for "{query}"?') is PermissionDecision.DENY:
|
|
191
|
+
return ToolResult.fail("web search denied by user")
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
results = _instant_answer(query, max_results)
|
|
195
|
+
if not results:
|
|
196
|
+
results = _html_search(query, max_results)
|
|
197
|
+
except _BotChallenge:
|
|
198
|
+
return ToolResult.fail(
|
|
199
|
+
"DuckDuckGo returned a bot-verification challenge (likely rate-limited "
|
|
200
|
+
"this IP). Wait a bit before retrying, or try a more specific query."
|
|
201
|
+
)
|
|
202
|
+
except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
|
203
|
+
return ToolResult.fail(f"web search failed: {exc}")
|
|
204
|
+
|
|
205
|
+
self._cache[cache_key] = (time.monotonic(), results)
|
|
206
|
+
return self._format(query, results)
|
|
207
|
+
|
|
208
|
+
def _format(self, query: str, results: list[dict[str, str]]) -> ToolResult:
|
|
209
|
+
if not results:
|
|
210
|
+
return ToolResult.ok_(f"no results for: {query}", count=0)
|
|
211
|
+
lines = [f"{i}. {r['title']}\n {r['url']}\n {r['snippet']}" for i, r in enumerate(results, 1)]
|
|
212
|
+
return ToolResult.ok_("\n".join(lines), count=len(results), results=results)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def register_web_tools(registry) -> None: # type: ignore[no-untyped-def]
|
|
216
|
+
registry.register(WebSearchTool())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from reidx.ui.commands import handle
|
|
2
|
+
from reidx.ui.render import (
|
|
3
|
+
banner,
|
|
4
|
+
print_assistant,
|
|
5
|
+
print_error,
|
|
6
|
+
print_info,
|
|
7
|
+
print_permissions,
|
|
8
|
+
print_sessions,
|
|
9
|
+
print_tasks,
|
|
10
|
+
print_thinking,
|
|
11
|
+
print_tool_calls,
|
|
12
|
+
print_tools,
|
|
13
|
+
print_transcript,
|
|
14
|
+
print_user,
|
|
15
|
+
print_workflow_steps,
|
|
16
|
+
print_workflows,
|
|
17
|
+
rule,
|
|
18
|
+
status_bar,
|
|
19
|
+
status_prompt,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"banner",
|
|
24
|
+
"handle",
|
|
25
|
+
"print_assistant",
|
|
26
|
+
"print_error",
|
|
27
|
+
"print_info",
|
|
28
|
+
"print_permissions",
|
|
29
|
+
"print_sessions",
|
|
30
|
+
"print_tasks",
|
|
31
|
+
"print_thinking",
|
|
32
|
+
"print_tool_calls",
|
|
33
|
+
"print_tools",
|
|
34
|
+
"print_transcript",
|
|
35
|
+
"print_user",
|
|
36
|
+
"print_workflow_steps",
|
|
37
|
+
"print_workflows",
|
|
38
|
+
"rule",
|
|
39
|
+
"status_bar",
|
|
40
|
+
"status_prompt",
|
|
41
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|