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.
@@ -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())
@@ -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")