brown-octopus 0.4.4__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.
- brown_octopus/__init__.py +24 -0
- brown_octopus/active_tools.py +39 -0
- brown_octopus/analyzer.py +296 -0
- brown_octopus/bootstrap.py +35 -0
- brown_octopus/cli.py +220 -0
- brown_octopus/config.py +44 -0
- brown_octopus/context_manager.py +62 -0
- brown_octopus/contracts.py +100 -0
- brown_octopus/errors.py +38 -0
- brown_octopus/index_store.py +146 -0
- brown_octopus/mcp_discovery.py +123 -0
- brown_octopus/model_store.py +90 -0
- brown_octopus/observability.py +24 -0
- brown_octopus/octopus.py +441 -0
- brown_octopus/pipeline.py +159 -0
- brown_octopus/py.typed +0 -0
- brown_octopus/retriever.py +192 -0
- brown_octopus/selection.py +74 -0
- brown_octopus/server.py +13 -0
- brown_octopus/service.py +54 -0
- brown_octopus/session_store.py +76 -0
- brown_octopus/setup_models.py +220 -0
- brown_octopus/sources.py +64 -0
- brown_octopus/strategies.py +91 -0
- brown_octopus/tool_registry.py +33 -0
- brown_octopus-0.4.4.dist-info/METADATA +404 -0
- brown_octopus-0.4.4.dist-info/RECORD +29 -0
- brown_octopus-0.4.4.dist-info/WHEEL +4 -0
- brown_octopus-0.4.4.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from brown_octopus.octopus import Octopus
|
|
2
|
+
from brown_octopus.config import OctopusConfig
|
|
3
|
+
from brown_octopus.contracts import RetrievalResult
|
|
4
|
+
from brown_octopus.contracts import (
|
|
5
|
+
CapabilityDiscoveryResult,
|
|
6
|
+
CapabilitySource,
|
|
7
|
+
SessionState,
|
|
8
|
+
SessionStore,
|
|
9
|
+
)
|
|
10
|
+
from brown_octopus.session_store import InMemorySessionStore
|
|
11
|
+
from brown_octopus.sources import CapabilityUpdateReport, LocalMcpCatalogSource
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Octopus",
|
|
15
|
+
"OctopusConfig",
|
|
16
|
+
"RetrievalResult",
|
|
17
|
+
"CapabilitySource",
|
|
18
|
+
"SessionState",
|
|
19
|
+
"SessionStore",
|
|
20
|
+
"InMemorySessionStore",
|
|
21
|
+
"CapabilityDiscoveryResult",
|
|
22
|
+
"CapabilityUpdateReport",
|
|
23
|
+
"LocalMcpCatalogSource",
|
|
24
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
ACTIVE_TOOL_TTL = 8
|
|
2
|
+
MAX_ACTIVE_TOOLS = 30
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def update_active_tools(
|
|
6
|
+
active_tools: dict[str, int],
|
|
7
|
+
retrieved_tools: list[str],
|
|
8
|
+
turn: int,
|
|
9
|
+
ttl: int = ACTIVE_TOOL_TTL,
|
|
10
|
+
max_active_tools: int = MAX_ACTIVE_TOOLS,
|
|
11
|
+
) -> dict[str, int]:
|
|
12
|
+
updated_tools = active_tools.copy()
|
|
13
|
+
|
|
14
|
+
# Add newly retrieved tools and refresh
|
|
15
|
+
# tools that were retrieved again.
|
|
16
|
+
for tool_name in retrieved_tools:
|
|
17
|
+
updated_tools[tool_name] = turn
|
|
18
|
+
|
|
19
|
+
# Remove tools that have exceeded their TTL.
|
|
20
|
+
updated_tools = {
|
|
21
|
+
tool_name: last_retrieved_turn
|
|
22
|
+
for tool_name, last_retrieved_turn in updated_tools.items()
|
|
23
|
+
if turn - last_retrieved_turn <= ttl
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# If the context is still too large,
|
|
27
|
+
# keep the most recently retrieved tools.
|
|
28
|
+
if len(updated_tools) > max_active_tools:
|
|
29
|
+
# When several capabilities were retrieved on the same turn, retain
|
|
30
|
+
# the later entries as the most recently encountered ones. This keeps
|
|
31
|
+
# the cap deterministic and evicts the oldest entry first.
|
|
32
|
+
indexed = list(enumerate(updated_tools.items()))
|
|
33
|
+
indexed.sort(key=lambda item: (item[1][1], item[0]), reverse=True)
|
|
34
|
+
updated_tools = dict(item for _, item in indexed[:max_active_tools])
|
|
35
|
+
|
|
36
|
+
return updated_tools
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import spacy
|
|
2
|
+
from spacy.language import Language
|
|
3
|
+
from spacy.tokens import Doc, Token
|
|
4
|
+
|
|
5
|
+
from brown_octopus.model_store import register_managed_spacy_plugins, resolve_spacy_model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SPACY_MODEL_NAME = "en_core_web_trf"
|
|
9
|
+
OBJECT_DEPENDENCIES = {"dobj", "obj", "attr"}
|
|
10
|
+
|
|
11
|
+
nlp: Language | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeterministicIntentAnalyzer:
|
|
15
|
+
"""Interface adapter for the current deterministic spaCy analyzer."""
|
|
16
|
+
|
|
17
|
+
name = "spacy_deterministic"
|
|
18
|
+
|
|
19
|
+
def analyze(self, text: str) -> list[dict]:
|
|
20
|
+
return analyze_intents(text)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def initialize_analyzer() -> None:
|
|
24
|
+
"""Load the spaCy model once."""
|
|
25
|
+
global nlp
|
|
26
|
+
|
|
27
|
+
if nlp is None:
|
|
28
|
+
register_managed_spacy_plugins()
|
|
29
|
+
nlp = spacy.load(resolve_spacy_model())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_direct_object(token: Token) -> bool:
|
|
33
|
+
return any(child.dep_ in OBJECT_DEPENDENCIES for child in token.children)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _find_action_tokens(doc: Doc) -> list[Token]:
|
|
37
|
+
root = next(token for token in doc if token.dep_ == "ROOT")
|
|
38
|
+
|
|
39
|
+
action_tokens = [root]
|
|
40
|
+
|
|
41
|
+
action_tokens.extend(
|
|
42
|
+
token for token in doc if token.dep_ == "conj" and token.pos_ == "VERB"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
action_tokens.extend(
|
|
46
|
+
token
|
|
47
|
+
for token in doc
|
|
48
|
+
if token.dep_ == "dep"
|
|
49
|
+
and token.pos_ == "VERB"
|
|
50
|
+
and token.head == root
|
|
51
|
+
and _has_direct_object(token)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
action_tokens.extend(
|
|
55
|
+
token
|
|
56
|
+
for token in doc
|
|
57
|
+
if token.dep_ == "acl" and token.pos_ == "VERB" and _has_direct_object(token)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return sorted(
|
|
61
|
+
set(action_tokens),
|
|
62
|
+
key=lambda token: token.i,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _belongs_to_nested_action(
|
|
67
|
+
token: Token,
|
|
68
|
+
action_token: Token,
|
|
69
|
+
action_tokens: list[Token],
|
|
70
|
+
) -> bool:
|
|
71
|
+
action_subtree = set(action_token.subtree)
|
|
72
|
+
|
|
73
|
+
return any(
|
|
74
|
+
other_action != action_token
|
|
75
|
+
and other_action in action_subtree
|
|
76
|
+
and (token == other_action or other_action in token.ancestors)
|
|
77
|
+
for other_action in action_tokens
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_local_text(
|
|
82
|
+
action_token: Token,
|
|
83
|
+
action_tokens: list[Token],
|
|
84
|
+
) -> str:
|
|
85
|
+
local_tokens = []
|
|
86
|
+
|
|
87
|
+
for token in action_token.subtree:
|
|
88
|
+
if token.is_punct:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if token.dep_ == "cc":
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
if _belongs_to_nested_action(
|
|
95
|
+
token,
|
|
96
|
+
action_token,
|
|
97
|
+
action_tokens,
|
|
98
|
+
):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
local_tokens.append(token)
|
|
102
|
+
|
|
103
|
+
local_tokens.sort(key=lambda token: token.i)
|
|
104
|
+
|
|
105
|
+
return " ".join(token.text for token in local_tokens)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_direct_target(
|
|
109
|
+
action_token: Token,
|
|
110
|
+
action_tokens: list[Token],
|
|
111
|
+
) -> str | None:
|
|
112
|
+
targets = []
|
|
113
|
+
|
|
114
|
+
for child in action_token.children:
|
|
115
|
+
if child.dep_ not in OBJECT_DEPENDENCIES:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
target_tokens = []
|
|
119
|
+
|
|
120
|
+
for token in child.subtree:
|
|
121
|
+
if token.is_punct:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if _belongs_to_nested_action(
|
|
125
|
+
token,
|
|
126
|
+
action_token,
|
|
127
|
+
action_tokens,
|
|
128
|
+
):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
target_tokens.append(token)
|
|
132
|
+
|
|
133
|
+
target_tokens.sort(key=lambda token: token.i)
|
|
134
|
+
|
|
135
|
+
target_text = " ".join(token.text for token in target_tokens).strip()
|
|
136
|
+
|
|
137
|
+
if target_text:
|
|
138
|
+
targets.append(target_text)
|
|
139
|
+
|
|
140
|
+
if not targets:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
return " ".join(targets)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _get_core_resource(action_token: Token) -> str | None:
|
|
147
|
+
"""Return the core grammatical resource operated on by an action.
|
|
148
|
+
|
|
149
|
+
This intentionally uses only dependency structure. Task-specific
|
|
150
|
+
arguments such as names, titles, and dates remain in the normal intent
|
|
151
|
+
fields but do not become part of the capability retrieval query.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def collect_compounds(token: Token) -> list[Token]:
|
|
155
|
+
compounds: list[Token] = []
|
|
156
|
+
for child in token.children:
|
|
157
|
+
if child.dep_ == "compound":
|
|
158
|
+
compounds.extend(collect_compounds(child))
|
|
159
|
+
compounds.append(child)
|
|
160
|
+
return compounds
|
|
161
|
+
|
|
162
|
+
for child in action_token.children:
|
|
163
|
+
if child.dep_ not in OBJECT_DEPENDENCIES:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
tokens = collect_compounds(child) + [child]
|
|
167
|
+
tokens.sort(key=lambda token: token.i)
|
|
168
|
+
resource = " ".join(token.text for token in tokens).strip()
|
|
169
|
+
if resource:
|
|
170
|
+
return resource
|
|
171
|
+
|
|
172
|
+
# Handle constructions such as "reply to the email".
|
|
173
|
+
for child in action_token.children:
|
|
174
|
+
if child.dep_ not in {"prep", "dative"}:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
for token in child.subtree:
|
|
178
|
+
if token.dep_ != "pobj" or token.pos_ not in {"NOUN", "PROPN"}:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
tokens = collect_compounds(token) + [token]
|
|
182
|
+
tokens.sort(key=lambda item: item.i)
|
|
183
|
+
resource = " ".join(item.text for item in tokens).strip()
|
|
184
|
+
if resource:
|
|
185
|
+
return resource
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _get_retrieval_text(action_token: Token) -> str:
|
|
191
|
+
"""Build the compact capability-oriented query for one intent."""
|
|
192
|
+
|
|
193
|
+
resource = _get_core_resource(action_token)
|
|
194
|
+
if not resource:
|
|
195
|
+
return action_token.lemma_
|
|
196
|
+
return f"{action_token.lemma_} {resource}"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _get_fallback_target(
|
|
200
|
+
doc: Doc,
|
|
201
|
+
action_token: Token,
|
|
202
|
+
action_tokens: list[Token],
|
|
203
|
+
) -> str | None:
|
|
204
|
+
action_index = action_token.i
|
|
205
|
+
|
|
206
|
+
later_actions = [token.i for token in action_tokens if token.i > action_index]
|
|
207
|
+
|
|
208
|
+
end_index = min(later_actions) if later_actions else len(doc)
|
|
209
|
+
|
|
210
|
+
candidates = [
|
|
211
|
+
token
|
|
212
|
+
for token in doc[action_index:end_index]
|
|
213
|
+
if token.pos_ in {"NOUN", "PROPN"}
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
if not candidates:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
return " ".join(token.text for token in candidates)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def analyze_intents(text: str) -> list[dict]:
|
|
223
|
+
"""
|
|
224
|
+
Decompose a user request into operational intents.
|
|
225
|
+
"""
|
|
226
|
+
if nlp is None:
|
|
227
|
+
raise RuntimeError(
|
|
228
|
+
"Analyzer is not initialized. "
|
|
229
|
+
"Call initialize() before processing requests."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
original_text = text.strip()
|
|
233
|
+
|
|
234
|
+
if not original_text:
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
normalized_text = original_text[0].upper() + original_text[1:]
|
|
238
|
+
|
|
239
|
+
doc = nlp(normalized_text)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
action_tokens = _find_action_tokens(doc)
|
|
243
|
+
except StopIteration:
|
|
244
|
+
# Some short, mathematical, or malformed benchmark strings can be
|
|
245
|
+
# parsed without a ROOT. Preserve the request as one opaque intent so
|
|
246
|
+
# retrieval remains available instead of crashing the service.
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
"action": "request",
|
|
250
|
+
"target": None,
|
|
251
|
+
"text": original_text,
|
|
252
|
+
"retrieval_text": original_text,
|
|
253
|
+
"context": original_text,
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
if not action_tokens:
|
|
258
|
+
return [
|
|
259
|
+
{
|
|
260
|
+
"action": "request",
|
|
261
|
+
"target": None,
|
|
262
|
+
"text": original_text,
|
|
263
|
+
"retrieval_text": original_text,
|
|
264
|
+
"context": original_text,
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
intents = []
|
|
269
|
+
|
|
270
|
+
for action_token in action_tokens:
|
|
271
|
+
target = _get_direct_target(
|
|
272
|
+
action_token,
|
|
273
|
+
action_tokens,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if target is None:
|
|
277
|
+
target = _get_fallback_target(
|
|
278
|
+
doc,
|
|
279
|
+
action_token,
|
|
280
|
+
action_tokens,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
intents.append(
|
|
284
|
+
{
|
|
285
|
+
"action": action_token.lemma_,
|
|
286
|
+
"target": target,
|
|
287
|
+
"text": _get_local_text(
|
|
288
|
+
action_token,
|
|
289
|
+
action_tokens,
|
|
290
|
+
),
|
|
291
|
+
"retrieval_text": _get_retrieval_text(action_token),
|
|
292
|
+
"context": original_text,
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return intents
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from brown_octopus.analyzer import initialize_analyzer
|
|
4
|
+
from brown_octopus.mcp_discovery import discover_universe
|
|
5
|
+
from brown_octopus.retriever import (
|
|
6
|
+
initialize_retriever,
|
|
7
|
+
refresh_index,
|
|
8
|
+
)
|
|
9
|
+
from brown_octopus.sources import load_mcp_urls
|
|
10
|
+
from brown_octopus.tool_registry import set_tools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_MCP_CATALOG = Path("data/mcps.json")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def initialize(
|
|
17
|
+
catalog_path: str | Path = DEFAULT_MCP_CATALOG,
|
|
18
|
+
) -> list[dict]:
|
|
19
|
+
"""
|
|
20
|
+
Initialize Brown Octopus and prepare it for requests.
|
|
21
|
+
|
|
22
|
+
Loads runtime models, discovers the available MCP tools,
|
|
23
|
+
populates the registry, and builds the retrieval index.
|
|
24
|
+
"""
|
|
25
|
+
initialize_analyzer()
|
|
26
|
+
initialize_retriever()
|
|
27
|
+
|
|
28
|
+
mcp_urls = load_mcp_urls(catalog_path)
|
|
29
|
+
|
|
30
|
+
tools = await discover_universe(mcp_urls)
|
|
31
|
+
|
|
32
|
+
set_tools(tools)
|
|
33
|
+
refresh_index()
|
|
34
|
+
|
|
35
|
+
return tools
|
brown_octopus/cli.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Small, dependency-light command line interface for Brown Octopus."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from brown_octopus.config import OctopusConfig
|
|
11
|
+
from brown_octopus.model_store import (
|
|
12
|
+
EMBEDDING_MODEL_NAME,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DESCRIPTION = "Capability context management for AI agents."
|
|
17
|
+
MODEL_DOCTOR_TIMEOUT_SECONDS = 180
|
|
18
|
+
EMBEDDING_DOCTOR_TIMEOUT_SECONDS = 120
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _version() -> str:
|
|
22
|
+
try:
|
|
23
|
+
return version("brown-octopus")
|
|
24
|
+
except PackageNotFoundError:
|
|
25
|
+
return "0.4.4"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_probe(code: str, timeout: int = 120) -> tuple[bool, str]:
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
[sys.executable, "-c", code],
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
37
|
+
return False, str(exc)
|
|
38
|
+
output = (result.stdout or result.stderr).strip()
|
|
39
|
+
return result.returncode == 0, output
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _index_details(index_path: Path) -> dict:
|
|
43
|
+
details = {"path": str(index_path), "exists": index_path.exists()}
|
|
44
|
+
if not index_path.exists():
|
|
45
|
+
return details
|
|
46
|
+
pointer = index_path / "current.json"
|
|
47
|
+
active = index_path
|
|
48
|
+
if pointer.exists():
|
|
49
|
+
try:
|
|
50
|
+
active = index_path / json.loads(pointer.read_text(encoding="utf-8"))["snapshot"]
|
|
51
|
+
except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
52
|
+
details["error"] = f"invalid current.json: {exc}"
|
|
53
|
+
return details
|
|
54
|
+
metadata_path = active / "metadata.json"
|
|
55
|
+
details.update(
|
|
56
|
+
{
|
|
57
|
+
"active_path": str(active),
|
|
58
|
+
"metadata": metadata_path.exists(),
|
|
59
|
+
"tools": (active / "tools.json").exists(),
|
|
60
|
+
"embeddings": (active / "embeddings.pt").exists(),
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
if metadata_path.exists():
|
|
64
|
+
try:
|
|
65
|
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
66
|
+
details.update(
|
|
67
|
+
{
|
|
68
|
+
"index_version": metadata.get("version"),
|
|
69
|
+
"tool_count": metadata.get("tool_count"),
|
|
70
|
+
"embedding_model": metadata.get("embedding_model"),
|
|
71
|
+
"brown_octopus_version": metadata.get("brown_octopus_version"),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
75
|
+
details["error"] = f"invalid metadata.json: {exc}"
|
|
76
|
+
return details
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _doctor() -> int:
|
|
80
|
+
config = OctopusConfig.from_env()
|
|
81
|
+
package_ok, package_output = _run_probe("import brown_octopus")
|
|
82
|
+
spacy_ok, _ = _run_probe(
|
|
83
|
+
"from brown_octopus.analyzer import initialize_analyzer; "
|
|
84
|
+
"initialize_analyzer(); print('en_core_web_trf')",
|
|
85
|
+
timeout=MODEL_DOCTOR_TIMEOUT_SECONDS,
|
|
86
|
+
)
|
|
87
|
+
qwen_ok, _ = _run_probe(
|
|
88
|
+
"from huggingface_hub import try_to_load_from_cache; "
|
|
89
|
+
f"p=try_to_load_from_cache({EMBEDDING_MODEL_NAME!r}, 'config.json'); "
|
|
90
|
+
"assert p is not None; print(p)",
|
|
91
|
+
timeout=EMBEDDING_DOCTOR_TIMEOUT_SECONDS,
|
|
92
|
+
)
|
|
93
|
+
index = _index_details(config.index_path)
|
|
94
|
+
index_ok = all(index.get(key, False) for key in ("metadata", "tools", "embeddings"))
|
|
95
|
+
index_ok = index_ok and index.get("index_version") == 1
|
|
96
|
+
healthy = package_ok and spacy_ok and qwen_ok and index_ok
|
|
97
|
+
|
|
98
|
+
print("Brown Octopus Doctor\n")
|
|
99
|
+
print("Package")
|
|
100
|
+
mark = lambda ok: "[OK]" if ok else "[FAIL]"
|
|
101
|
+
print(f" {mark(package_ok)} brown-octopus {_version()}")
|
|
102
|
+
print(f" {mark(package_ok)} Python {sys.version.split()[0]}")
|
|
103
|
+
print("\nModels")
|
|
104
|
+
print(f" {mark(spacy_ok)} en_core_web_trf")
|
|
105
|
+
print(f" {mark(qwen_ok)} Qwen/Qwen3-Embedding-0.6B")
|
|
106
|
+
print("\nIndex")
|
|
107
|
+
print(f" {mark(index.get('exists', False))} index found: {config.index_path}")
|
|
108
|
+
print(f" {mark(index_ok)} compatible index version")
|
|
109
|
+
if index.get("tool_count") is not None:
|
|
110
|
+
print(f" {mark(index_ok)} {index['tool_count']} capabilities")
|
|
111
|
+
if not healthy:
|
|
112
|
+
print("\nFix:")
|
|
113
|
+
if not spacy_ok or not qwen_ok:
|
|
114
|
+
print(" brown-octopus setup-models")
|
|
115
|
+
if not index_ok:
|
|
116
|
+
print(" Build or provide a valid index with: await octopus.update()")
|
|
117
|
+
if not package_ok and package_output:
|
|
118
|
+
print(f" Import diagnostic: {package_output.splitlines()[-1]}")
|
|
119
|
+
print("\nStatus\n [FAIL] Brown Octopus is not ready.")
|
|
120
|
+
return 1
|
|
121
|
+
print("\nStatus\n [OK] Brown Octopus is ready.")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _inspect(as_json: bool) -> int:
|
|
126
|
+
config = OctopusConfig.from_env()
|
|
127
|
+
index = _index_details(config.index_path)
|
|
128
|
+
data = {
|
|
129
|
+
"product": "Brown Octopus",
|
|
130
|
+
"version": _version(),
|
|
131
|
+
"capabilities": index.get("tool_count"),
|
|
132
|
+
"index_version": index.get("index_version"),
|
|
133
|
+
"index_path": str(config.index_path),
|
|
134
|
+
"embedding_model": index.get("embedding_model") or EMBEDDING_MODEL_NAME,
|
|
135
|
+
"analyzer": "deterministic-spacy",
|
|
136
|
+
"selector": "V3 Min-4 + Bounded Max Gap",
|
|
137
|
+
"ttl": config.ttl,
|
|
138
|
+
"active_cap": config.active_cap,
|
|
139
|
+
}
|
|
140
|
+
if as_json:
|
|
141
|
+
print(json.dumps(data, indent=2))
|
|
142
|
+
return 0
|
|
143
|
+
print("Brown Octopus\n")
|
|
144
|
+
for key, label in (
|
|
145
|
+
("version", "Version"),
|
|
146
|
+
("capabilities", "Capabilities"),
|
|
147
|
+
("index_version", "Index version"),
|
|
148
|
+
("embedding_model", "Embedding"),
|
|
149
|
+
("analyzer", "Analyzer"),
|
|
150
|
+
("selector", "Selector"),
|
|
151
|
+
("ttl", "TTL"),
|
|
152
|
+
("active_cap", "Active cap"),
|
|
153
|
+
):
|
|
154
|
+
print(f"{label + ':':16} {data[key]}")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
159
|
+
parser = argparse.ArgumentParser(
|
|
160
|
+
prog="brown-octopus",
|
|
161
|
+
usage="brown-octopus COMMAND",
|
|
162
|
+
description=f"Brown Octopus - {DESCRIPTION}",
|
|
163
|
+
epilog=(
|
|
164
|
+
"Quick start:\n"
|
|
165
|
+
" pip install brown-octopus\n"
|
|
166
|
+
" brown-octopus setup-models\n"
|
|
167
|
+
" from brown_octopus import Octopus"
|
|
168
|
+
),
|
|
169
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {_version()}")
|
|
172
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
173
|
+
setup = subparsers.add_parser(
|
|
174
|
+
"setup-models",
|
|
175
|
+
help="Prepare the models required by Octopus.",
|
|
176
|
+
description=(
|
|
177
|
+
"Download and validate en_core_web_trf and "
|
|
178
|
+
"Qwen/Qwen3-Embedding-0.6B.\n"
|
|
179
|
+
"Model setup is explicit; initialize() never downloads models."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
setup.set_defaults(handler="setup-models")
|
|
183
|
+
doctor = subparsers.add_parser("doctor", help="Check whether Brown Octopus is ready.")
|
|
184
|
+
doctor.set_defaults(handler="doctor")
|
|
185
|
+
inspect = subparsers.add_parser(
|
|
186
|
+
"inspect", help="Inspect the local index and runtime configuration."
|
|
187
|
+
)
|
|
188
|
+
inspect.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
|
|
189
|
+
inspect.set_defaults(handler="inspect")
|
|
190
|
+
version_parser = subparsers.add_parser("version", help="Display version information.")
|
|
191
|
+
version_parser.set_defaults(handler="version")
|
|
192
|
+
return parser
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main() -> int:
|
|
196
|
+
parser = _build_parser()
|
|
197
|
+
args = parser.parse_args()
|
|
198
|
+
handler = getattr(args, "handler", None)
|
|
199
|
+
try:
|
|
200
|
+
if handler == "setup-models":
|
|
201
|
+
from brown_octopus.setup_models import main as setup_models
|
|
202
|
+
|
|
203
|
+
setup_models()
|
|
204
|
+
return 0
|
|
205
|
+
if handler == "doctor":
|
|
206
|
+
return _doctor()
|
|
207
|
+
if handler == "inspect":
|
|
208
|
+
return _inspect(args.json)
|
|
209
|
+
if handler == "version":
|
|
210
|
+
print(f"brown-octopus {_version()}")
|
|
211
|
+
return 0
|
|
212
|
+
parser.print_help()
|
|
213
|
+
return 0
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
print(f"Brown Octopus: {exc}", file=sys.stderr)
|
|
216
|
+
return 1
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
raise SystemExit(main())
|
brown_octopus/config.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Runtime configuration loaded from environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _int(name: str, default: int) -> int:
|
|
9
|
+
value = os.getenv(name)
|
|
10
|
+
return default if value is None else int(value)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _float(name: str, default: float) -> float:
|
|
14
|
+
value = os.getenv(name)
|
|
15
|
+
return default if value is None else float(value)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class OctopusConfig:
|
|
20
|
+
index_path: Path = Path("data/indexes/default")
|
|
21
|
+
catalog_path: Path = Path("data/mcps.json")
|
|
22
|
+
ttl: int = 8
|
|
23
|
+
active_cap: int = 30
|
|
24
|
+
min_tools: int = 4
|
|
25
|
+
max_tools: int = 16
|
|
26
|
+
min_gap_percent: float = 2.0
|
|
27
|
+
log_level: str = "INFO"
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_env(cls) -> "OctopusConfig":
|
|
31
|
+
return cls(
|
|
32
|
+
index_path=Path(os.getenv("OCTOPUS_INDEX_PATH", "data/indexes/default")),
|
|
33
|
+
catalog_path=Path(os.getenv("OCTOPUS_CATALOG_PATH", "data/mcps.json")),
|
|
34
|
+
ttl=_int("OCTOPUS_CAPABILITY_TTL", 8),
|
|
35
|
+
active_cap=_int("OCTOPUS_ACTIVE_CAP", 30),
|
|
36
|
+
min_tools=_int("OCTOPUS_MIN_TOOLS", 4),
|
|
37
|
+
max_tools=_int("OCTOPUS_MAX_TOOLS", 16),
|
|
38
|
+
min_gap_percent=_float("OCTOPUS_MIN_GAP_PERCENT", 2.0),
|
|
39
|
+
log_level=os.getenv("OCTOPUS_LOG_LEVEL", "INFO"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def validate(self) -> None:
|
|
43
|
+
if self.ttl < 0 or self.active_cap < 1:
|
|
44
|
+
raise ValueError("TTL and active capability cap must be valid")
|