@trac3er/oh-my-god 1.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/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +119 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Brave Search Provider for OMG
|
|
4
|
+
|
|
5
|
+
Uses the Brave Search API (https://api.search.brave.com/res/v1/web/search).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
_tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
17
|
+
if _tools_dir not in sys.path:
|
|
18
|
+
sys.path.insert(0, _tools_dir)
|
|
19
|
+
|
|
20
|
+
from web_search import Provider, SearchResult, get_api_key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BraveProvider(Provider):
|
|
24
|
+
"""Search provider using the Brave Search API.
|
|
25
|
+
|
|
26
|
+
Endpoint: https://api.search.brave.com/res/v1/web/search
|
|
27
|
+
Requires an API key (BRAVE_API_KEY env var or credential store).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
CONFIG_SCHEMA: Dict[str, Any] = {
|
|
31
|
+
"api_key": {"type": "str", "required": True},
|
|
32
|
+
"count": {"type": "int", "required": False, "default": 10},
|
|
33
|
+
"country": {"type": "str", "required": False, "default": ""},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
API_URL = "https://api.search.brave.com/res/v1/web/search"
|
|
37
|
+
|
|
38
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
39
|
+
"""Initialize BraveProvider.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_key: Brave API key. If None, resolves via get_api_key('brave').
|
|
43
|
+
"""
|
|
44
|
+
self._api_key = api_key or get_api_key("brave")
|
|
45
|
+
|
|
46
|
+
def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
|
|
47
|
+
"""Search using the Brave Search API.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
query: The search query string.
|
|
51
|
+
**kwargs: Optional 'count' (default 10), 'country'.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A list of result dicts, or empty list on error.
|
|
55
|
+
"""
|
|
56
|
+
if not self._api_key:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
count = kwargs.get("count", 10)
|
|
60
|
+
params = {"q": query, "count": str(count)}
|
|
61
|
+
country = kwargs.get("country", "")
|
|
62
|
+
if country:
|
|
63
|
+
params["country"] = country
|
|
64
|
+
|
|
65
|
+
url = f"{self.API_URL}?{urllib.parse.urlencode(params)}"
|
|
66
|
+
req = urllib.request.Request(
|
|
67
|
+
url,
|
|
68
|
+
headers={
|
|
69
|
+
"Accept": "application/json",
|
|
70
|
+
"Accept-Encoding": "gzip",
|
|
71
|
+
"X-Subscription-Token": self._api_key,
|
|
72
|
+
"User-Agent": "OMG-WebSearch/1.0",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
78
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
79
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
80
|
+
return []
|
|
81
|
+
except Exception:
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
results = []
|
|
85
|
+
web_results = data.get("web", {}).get("results", [])
|
|
86
|
+
for item in web_results:
|
|
87
|
+
results.append({
|
|
88
|
+
"title": item.get("title", ""),
|
|
89
|
+
"url": item.get("url", ""),
|
|
90
|
+
"snippet": item.get("description", ""),
|
|
91
|
+
"source": "brave",
|
|
92
|
+
})
|
|
93
|
+
return results
|
|
94
|
+
|
|
95
|
+
def fetch(self, url: str) -> str:
|
|
96
|
+
"""Fetch URL content using stdlib urllib.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
url: The URL to fetch.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Page content as string, or empty string on error.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
req = urllib.request.Request(
|
|
106
|
+
url,
|
|
107
|
+
headers={"User-Agent": "OMG-WebSearch/1.0"},
|
|
108
|
+
)
|
|
109
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
110
|
+
encoding = resp.headers.get_content_charset() or "utf-8"
|
|
111
|
+
return resp.read().decode(encoding, errors="replace")
|
|
112
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
113
|
+
return ""
|
|
114
|
+
except Exception:
|
|
115
|
+
return ""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Exa Search Provider for OMG
|
|
4
|
+
|
|
5
|
+
Uses the Exa AI search API (https://api.exa.ai/search) for semantic web search.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
_tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
if _tools_dir not in sys.path:
|
|
17
|
+
sys.path.insert(0, _tools_dir)
|
|
18
|
+
|
|
19
|
+
from web_search import Provider, SearchResult, get_api_key
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExaProvider(Provider):
|
|
23
|
+
"""Search provider using the Exa AI search API.
|
|
24
|
+
|
|
25
|
+
Endpoint: https://api.exa.ai/search
|
|
26
|
+
Requires an API key (EXA_API_KEY env var or credential store).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
CONFIG_SCHEMA: Dict[str, Any] = {
|
|
30
|
+
"api_key": {"type": "str", "required": True},
|
|
31
|
+
"num_results": {"type": "int", "required": False, "default": 10},
|
|
32
|
+
"use_autoprompt": {"type": "bool", "required": False, "default": True},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
API_URL = "https://api.exa.ai/search"
|
|
36
|
+
|
|
37
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
38
|
+
"""Initialize ExaProvider.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
api_key: Exa API key. If None, resolves via get_api_key('exa').
|
|
42
|
+
"""
|
|
43
|
+
self._api_key = api_key or get_api_key("exa")
|
|
44
|
+
|
|
45
|
+
def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
|
|
46
|
+
"""Search using the Exa AI API.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
query: The search query string.
|
|
50
|
+
**kwargs: Optional 'num_results' (default 10), 'use_autoprompt'.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A list of result dicts, or empty list on error.
|
|
54
|
+
"""
|
|
55
|
+
if not self._api_key:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
num_results = kwargs.get("num_results", 10)
|
|
59
|
+
use_autoprompt = kwargs.get("use_autoprompt", True)
|
|
60
|
+
|
|
61
|
+
payload = json.dumps({
|
|
62
|
+
"query": query,
|
|
63
|
+
"numResults": num_results,
|
|
64
|
+
"useAutoprompt": use_autoprompt,
|
|
65
|
+
}).encode("utf-8")
|
|
66
|
+
|
|
67
|
+
req = urllib.request.Request(
|
|
68
|
+
self.API_URL,
|
|
69
|
+
data=payload,
|
|
70
|
+
headers={
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"x-api-key": self._api_key,
|
|
73
|
+
"User-Agent": "OMG-WebSearch/1.0",
|
|
74
|
+
},
|
|
75
|
+
method="POST",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
80
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
81
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
82
|
+
return []
|
|
83
|
+
except Exception:
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
results = []
|
|
87
|
+
for item in data.get("results", []):
|
|
88
|
+
results.append({
|
|
89
|
+
"title": item.get("title", ""),
|
|
90
|
+
"url": item.get("url", ""),
|
|
91
|
+
"snippet": item.get("text", item.get("snippet", "")),
|
|
92
|
+
"source": "exa",
|
|
93
|
+
})
|
|
94
|
+
return results
|
|
95
|
+
|
|
96
|
+
def fetch(self, url: str) -> str:
|
|
97
|
+
"""Fetch URL content using stdlib urllib.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
url: The URL to fetch.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Page content as string, or empty string on error.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
req = urllib.request.Request(
|
|
107
|
+
url,
|
|
108
|
+
headers={"User-Agent": "OMG-WebSearch/1.0"},
|
|
109
|
+
)
|
|
110
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
111
|
+
encoding = resp.headers.get_content_charset() or "utf-8"
|
|
112
|
+
return resp.read().decode(encoding, errors="replace")
|
|
113
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
114
|
+
return ""
|
|
115
|
+
except Exception:
|
|
116
|
+
return ""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Jina Reader Provider for OMG
|
|
4
|
+
|
|
5
|
+
Uses the Jina Reader API (https://r.jina.ai/) for URL-based content extraction.
|
|
6
|
+
Jina Reader converts web pages to clean, readable text — it is primarily a
|
|
7
|
+
fetch/reader tool rather than a traditional search engine.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
_tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
18
|
+
if _tools_dir not in sys.path:
|
|
19
|
+
sys.path.insert(0, _tools_dir)
|
|
20
|
+
|
|
21
|
+
from web_search import Provider, SearchResult, get_api_key
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JinaProvider(Provider):
|
|
25
|
+
"""Content extraction provider using Jina Reader API.
|
|
26
|
+
|
|
27
|
+
Endpoint: https://r.jina.ai/{url}
|
|
28
|
+
Optionally accepts an API key (JINA_API_KEY) for higher rate limits.
|
|
29
|
+
Primary use is fetch() — search() builds a reader URL from the query.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
CONFIG_SCHEMA: Dict[str, Any] = {
|
|
33
|
+
"api_key": {"type": "str", "required": False},
|
|
34
|
+
"return_format": {"type": "str", "required": False, "default": "text"},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BASE_URL = "https://r.jina.ai/"
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
40
|
+
"""Initialize JinaProvider.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
api_key: Jina API key. Optional — Jina works without a key
|
|
44
|
+
but has lower rate limits.
|
|
45
|
+
"""
|
|
46
|
+
self._api_key = api_key or get_api_key("jina")
|
|
47
|
+
|
|
48
|
+
def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
|
|
49
|
+
"""Search by treating the query as a URL to read via Jina.
|
|
50
|
+
|
|
51
|
+
If the query looks like a URL, fetches it through Jina Reader.
|
|
52
|
+
Otherwise, returns an empty list (Jina Reader is URL-based, not
|
|
53
|
+
a search engine).
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
query: A URL string to read, or a search query (returns empty).
|
|
57
|
+
**kwargs: Optional 'return_format' ('text' or 'markdown').
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A list with one result dict if query is a URL, else empty list.
|
|
61
|
+
"""
|
|
62
|
+
# Jina Reader is URL-based — only process URLs
|
|
63
|
+
if not query.startswith(("http://", "https://")):
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
content = self.fetch(query)
|
|
67
|
+
if not content:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
return [{
|
|
71
|
+
"title": f"Jina Reader: {query[:80]}",
|
|
72
|
+
"url": query,
|
|
73
|
+
"snippet": content[:500],
|
|
74
|
+
"source": "jina",
|
|
75
|
+
}]
|
|
76
|
+
|
|
77
|
+
def fetch(self, url: str) -> str:
|
|
78
|
+
"""Fetch and extract clean text from a URL using Jina Reader.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
url: The URL to fetch and extract content from.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Clean text content, or empty string on error.
|
|
85
|
+
"""
|
|
86
|
+
reader_url = f"{self.BASE_URL}{url}"
|
|
87
|
+
|
|
88
|
+
headers = {
|
|
89
|
+
"Accept": "text/plain",
|
|
90
|
+
"User-Agent": "OMG-WebSearch/1.0",
|
|
91
|
+
}
|
|
92
|
+
if self._api_key:
|
|
93
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
94
|
+
|
|
95
|
+
req = urllib.request.Request(reader_url, headers=headers)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
99
|
+
encoding = resp.headers.get_content_charset() or "utf-8"
|
|
100
|
+
return resp.read().decode(encoding, errors="replace")
|
|
101
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
102
|
+
return ""
|
|
103
|
+
except Exception:
|
|
104
|
+
return ""
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Perplexity Search Provider for OMG
|
|
4
|
+
|
|
5
|
+
Uses the Perplexity AI chat completions API
|
|
6
|
+
(https://api.perplexity.ai/chat/completions) for AI-powered web search.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
_tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
17
|
+
if _tools_dir not in sys.path:
|
|
18
|
+
sys.path.insert(0, _tools_dir)
|
|
19
|
+
|
|
20
|
+
from web_search import Provider, SearchResult, get_api_key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PerplexityProvider(Provider):
|
|
24
|
+
"""Search provider using the Perplexity AI API.
|
|
25
|
+
|
|
26
|
+
Endpoint: https://api.perplexity.ai/chat/completions
|
|
27
|
+
Requires an API key (PERPLEXITY_API_KEY env var or credential store).
|
|
28
|
+
Uses the sonar model for online search-augmented responses.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
CONFIG_SCHEMA: Dict[str, Any] = {
|
|
32
|
+
"api_key": {"type": "str", "required": True},
|
|
33
|
+
"model": {"type": "str", "required": False, "default": "sonar"},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
API_URL = "https://api.perplexity.ai/chat/completions"
|
|
37
|
+
|
|
38
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
39
|
+
"""Initialize PerplexityProvider.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_key: Perplexity API key. If None, resolves via
|
|
43
|
+
get_api_key('perplexity').
|
|
44
|
+
"""
|
|
45
|
+
self._api_key = api_key or get_api_key("perplexity")
|
|
46
|
+
|
|
47
|
+
def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
|
|
48
|
+
"""Search using the Perplexity AI API.
|
|
49
|
+
|
|
50
|
+
Sends the query as a chat message and parses citations from the
|
|
51
|
+
response into search result format.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
query: The search query string.
|
|
55
|
+
**kwargs: Optional 'model' (default 'sonar').
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A list of result dicts, or empty list on error.
|
|
59
|
+
"""
|
|
60
|
+
if not self._api_key:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
model = kwargs.get("model", "sonar")
|
|
64
|
+
|
|
65
|
+
payload = json.dumps({
|
|
66
|
+
"model": model,
|
|
67
|
+
"messages": [
|
|
68
|
+
{"role": "user", "content": query},
|
|
69
|
+
],
|
|
70
|
+
}).encode("utf-8")
|
|
71
|
+
|
|
72
|
+
req = urllib.request.Request(
|
|
73
|
+
self.API_URL,
|
|
74
|
+
data=payload,
|
|
75
|
+
headers={
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
78
|
+
"User-Agent": "OMG-WebSearch/1.0",
|
|
79
|
+
},
|
|
80
|
+
method="POST",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
85
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
86
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
87
|
+
return []
|
|
88
|
+
except Exception:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
results = []
|
|
92
|
+
# Extract answer text
|
|
93
|
+
answer = ""
|
|
94
|
+
choices = data.get("choices", [])
|
|
95
|
+
if choices:
|
|
96
|
+
answer = choices[0].get("message", {}).get("content", "")
|
|
97
|
+
|
|
98
|
+
# Extract citations if available
|
|
99
|
+
citations = data.get("citations", [])
|
|
100
|
+
if citations:
|
|
101
|
+
for i, url in enumerate(citations):
|
|
102
|
+
results.append({
|
|
103
|
+
"title": f"Citation {i + 1}",
|
|
104
|
+
"url": url if isinstance(url, str) else str(url),
|
|
105
|
+
"snippet": answer[:200] if i == 0 else "",
|
|
106
|
+
"source": "perplexity",
|
|
107
|
+
})
|
|
108
|
+
elif answer:
|
|
109
|
+
# No citations — return answer as a single result
|
|
110
|
+
results.append({
|
|
111
|
+
"title": f"Perplexity answer for: {query[:80]}",
|
|
112
|
+
"url": "",
|
|
113
|
+
"snippet": answer[:500],
|
|
114
|
+
"source": "perplexity",
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return results
|
|
118
|
+
|
|
119
|
+
def fetch(self, url: str) -> str:
|
|
120
|
+
"""Fetch URL content using stdlib urllib.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
url: The URL to fetch.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Page content as string, or empty string on error.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
req = urllib.request.Request(
|
|
130
|
+
url,
|
|
131
|
+
headers={"User-Agent": "OMG-WebSearch/1.0"},
|
|
132
|
+
)
|
|
133
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
134
|
+
encoding = resp.headers.get_content_charset() or "utf-8"
|
|
135
|
+
return resp.read().decode(encoding, errors="replace")
|
|
136
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
137
|
+
return ""
|
|
138
|
+
except Exception:
|
|
139
|
+
return ""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Synthetic Search Provider for OMG
|
|
4
|
+
|
|
5
|
+
Returns mock/fake results without making any API calls.
|
|
6
|
+
Useful for testing, dry-run mode, and offline development.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
# Ensure tools dir is on path for imports
|
|
14
|
+
_tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
15
|
+
if _tools_dir not in sys.path:
|
|
16
|
+
sys.path.insert(0, _tools_dir)
|
|
17
|
+
|
|
18
|
+
from web_search import Provider, SearchResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SyntheticProvider(Provider):
|
|
22
|
+
"""Mock search provider that returns fake results without API calls.
|
|
23
|
+
|
|
24
|
+
Designed for testing, dry-run mode, and offline development.
|
|
25
|
+
Does not require any API key or network access.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
CONFIG_SCHEMA: Dict[str, Any] = {
|
|
29
|
+
"api_key": {"type": "str", "required": False},
|
|
30
|
+
"num_results": {"type": "int", "required": False, "default": 3},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
34
|
+
"""Initialize SyntheticProvider.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
api_key: Ignored — synthetic provider needs no credentials.
|
|
38
|
+
"""
|
|
39
|
+
self._api_key = api_key # Accepted but unused
|
|
40
|
+
|
|
41
|
+
def search(self, query: str, **kwargs: Any) -> List[Dict[str, str]]:
|
|
42
|
+
"""Return synthetic search results for any query.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
query: The search query string.
|
|
46
|
+
**kwargs: Optional 'num_results' (default 3).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A list of fake result dicts with title, url, snippet keys.
|
|
50
|
+
"""
|
|
51
|
+
num_results = kwargs.get("num_results", 3)
|
|
52
|
+
results = []
|
|
53
|
+
for i in range(1, num_results + 1):
|
|
54
|
+
results.append({
|
|
55
|
+
"title": f"Synthetic Result {i} for: {query}",
|
|
56
|
+
"url": f"https://synthetic.example.com/result/{i}",
|
|
57
|
+
"snippet": f"This is a synthetic snippet #{i} for query '{query}'.",
|
|
58
|
+
"source": "synthetic",
|
|
59
|
+
})
|
|
60
|
+
return results
|
|
61
|
+
|
|
62
|
+
def fetch(self, url: str) -> str:
|
|
63
|
+
"""Return synthetic page content for any URL.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
url: The URL to 'fetch'.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A synthetic HTML string mentioning the URL.
|
|
70
|
+
"""
|
|
71
|
+
return (
|
|
72
|
+
f"<html><head><title>Synthetic Page</title></head>"
|
|
73
|
+
f"<body><p>Synthetic content for {url}</p></body></html>"
|
|
74
|
+
)
|