@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.
Files changed (135) hide show
  1. package/README.md +724 -0
  2. package/bin/reidx.js +33 -0
  3. package/package.json +40 -0
  4. package/pyproject.toml +45 -0
  5. package/scripts/find-python.js +24 -0
  6. package/scripts/postinstall.js +35 -0
  7. package/src/reidx/__init__.py +3 -0
  8. package/src/reidx/__main__.py +9 -0
  9. package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
  10. package/src/reidx/app/__init__.py +3 -0
  11. package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
  13. package/src/reidx/app/commands.py +221 -0
  14. package/src/reidx/automation/__init__.py +3 -0
  15. package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  16. package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
  17. package/src/reidx/automation/exec.py +35 -0
  18. package/src/reidx/config/__init__.py +10 -0
  19. package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
  21. package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
  22. package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
  23. package/src/reidx/config/loader.py +104 -0
  24. package/src/reidx/config/models.py +49 -0
  25. package/src/reidx/config/settings.py +127 -0
  26. package/src/reidx/deepreid/__init__.py +8 -0
  27. package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
  28. package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
  29. package/src/reidx/deepreid/pipeline.py +221 -0
  30. package/src/reidx/diagnostics/__init__.py +0 -0
  31. package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
  33. package/src/reidx/diagnostics/logger.py +56 -0
  34. package/src/reidx/goals/__init__.py +21 -0
  35. package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
  37. package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
  38. package/src/reidx/goals/models.py +101 -0
  39. package/src/reidx/goals/store.py +233 -0
  40. package/src/reidx/integrations/__init__.py +3 -0
  41. package/src/reidx/integrations/mcp.py +70 -0
  42. package/src/reidx/nyx/__init__.py +3 -0
  43. package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
  45. package/src/reidx/nyx/persona.py +35 -0
  46. package/src/reidx/policy/__init__.py +15 -0
  47. package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
  48. package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
  49. package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
  50. package/src/reidx/policy/engine.py +101 -0
  51. package/src/reidx/policy/models.py +35 -0
  52. package/src/reidx/provider/__init__.py +16 -0
  53. package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
  55. package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
  56. package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
  57. package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
  58. package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
  59. package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
  60. package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
  61. package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
  62. package/src/reidx/provider/_http.py +29 -0
  63. package/src/reidx/provider/anthropic.py +176 -0
  64. package/src/reidx/provider/base.py +50 -0
  65. package/src/reidx/provider/ollama.py +109 -0
  66. package/src/reidx/provider/openai.py +144 -0
  67. package/src/reidx/provider/registry.py +88 -0
  68. package/src/reidx/provider/store.py +141 -0
  69. package/src/reidx/provider/stub.py +60 -0
  70. package/src/reidx/provider_manager/__init__.py +40 -0
  71. package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
  73. package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
  74. package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
  75. package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
  76. package/src/reidx/provider_manager/catalog.py +330 -0
  77. package/src/reidx/provider_manager/database.py +234 -0
  78. package/src/reidx/provider_manager/keychain.py +102 -0
  79. package/src/reidx/provider_manager/palette.py +831 -0
  80. package/src/reidx/runtime/__init__.py +5 -0
  81. package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  82. package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
  83. package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
  84. package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
  85. package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
  86. package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
  87. package/src/reidx/runtime/agent.py +198 -0
  88. package/src/reidx/runtime/orchestrator.py +244 -0
  89. package/src/reidx/runtime/reasoning.py +77 -0
  90. package/src/reidx/runtime/state.py +32 -0
  91. package/src/reidx/runtime/subagent.py +174 -0
  92. package/src/reidx/session/__init__.py +4 -0
  93. package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
  94. package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
  95. package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
  96. package/src/reidx/session/models.py +43 -0
  97. package/src/reidx/session/store.py +101 -0
  98. package/src/reidx/tasks/__init__.py +4 -0
  99. package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
  100. package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
  101. package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
  102. package/src/reidx/tasks/models.py +44 -0
  103. package/src/reidx/tasks/store.py +86 -0
  104. package/src/reidx/tools/__init__.py +44 -0
  105. package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
  107. package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  108. package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
  109. package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
  110. package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
  111. package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  112. package/src/reidx/tools/base.py +84 -0
  113. package/src/reidx/tools/file_tools.py +222 -0
  114. package/src/reidx/tools/registry.py +45 -0
  115. package/src/reidx/tools/shell_tool.py +75 -0
  116. package/src/reidx/tools/spawn_agent.py +185 -0
  117. package/src/reidx/tools/web_tools.py +216 -0
  118. package/src/reidx/ui/__init__.py +41 -0
  119. package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
  120. package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
  121. package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
  122. package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
  123. package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
  124. package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
  125. package/src/reidx/ui/app.py +1343 -0
  126. package/src/reidx/ui/commands.py +663 -0
  127. package/src/reidx/ui/render.py +517 -0
  128. package/src/reidx/ui/repl.py +14 -0
  129. package/src/reidx/ui/theme.py +126 -0
  130. package/src/reidx/workflows/__init__.py +4 -0
  131. package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  132. package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
  133. package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
  134. package/src/reidx/workflows/models.py +19 -0
  135. 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
+ ]