@melaya/runner 1.0.85 → 1.0.87

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.
@@ -46,6 +46,55 @@ _stream = {"turnId": "", "lens": {}, "cancel": False}
46
46
  _CANCELLED = object()
47
47
 
48
48
 
49
+ class _RedactingStdout:
50
+ """Wrap stdout to strip long base64 runs before they reach the log.
51
+
52
+ agentscope prints every agent message verbatim (the `MelayaAssistant: {...}`
53
+ lines), which for phone_screenshot tool results embeds a full base64 JPEG —
54
+ tens of KB per turn that floods and explodes the runner logs. This filter is
55
+ line-buffered and only replaces base64-looking runs (>=200 chars), so the
56
+ structured `MELASSIST ` event lines (short text deltas / tool names) pass
57
+ through byte-for-byte and the runner's line parser is unaffected."""
58
+
59
+ import re as _re
60
+ _B64 = _re.compile(r"[A-Za-z0-9+/]{200,}={0,2}")
61
+
62
+ def __init__(self, real):
63
+ self._real = real
64
+ self._buf = ""
65
+
66
+ def _scrub(self, s: str) -> str:
67
+ return self._B64.sub(lambda m: f"<redacted base64 {len(m.group(0))} bytes>", s)
68
+
69
+ def write(self, s):
70
+ try:
71
+ self._buf += s
72
+ while "\n" in self._buf:
73
+ line, self._buf = self._buf.split("\n", 1)
74
+ self._real.write(self._scrub(line) + "\n")
75
+ return len(s)
76
+ except Exception:
77
+ return self._real.write(s)
78
+
79
+ def flush(self):
80
+ try:
81
+ if self._buf:
82
+ self._real.write(self._scrub(self._buf))
83
+ self._buf = ""
84
+ self._real.flush()
85
+ except Exception:
86
+ pass
87
+
88
+ def __getattr__(self, k):
89
+ return getattr(self._real, k)
90
+
91
+
92
+ # Install the redactor as early as possible so agentscope's message printing is
93
+ # filtered for the whole process lifetime.
94
+ if not isinstance(sys.stdout, _RedactingStdout):
95
+ sys.stdout = _RedactingStdout(sys.stdout)
96
+
97
+
49
98
  def _emit(turn_id: str, kind: str, **fields) -> None:
50
99
  """Write one structured event line to stdout (flushed) for the runner to relay."""
51
100
  try:
@@ -113,10 +162,25 @@ def _build_agent():
113
162
  # mobile only. These POST to /api/v1/private/assistant-tool + /phone/command
114
163
  # with MELAYA_API_KEY; tenant scope is enforced server-side.
115
164
  categories = ["melaya_agent"] + (["phone"] if phone_enabled else [])
165
+ # Connector tool sets the user enabled for THIS chat (any service — odoo,
166
+ # stripe, shopify, …). When present, use a LAZY toolkit: base tools stay
167
+ # active+pinned, and the connectors' tools are deferred + discoverable via
168
+ # search_tools/activate_tool (≤25 active) so even a 178-tool connector never
169
+ # explodes context. Bounded to the selected services so the model can't reach
170
+ # a connector the user didn't enable / has no creds for.
171
+ connector_services = [s.strip().lower() for s in os.environ.get("MEL_ASSISTANT_CONNECTORS", "").split(",") if s.strip()]
116
172
  try:
117
- toolkit = build_toolkit(categories=categories)
173
+ if connector_services:
174
+ from shared.orchestration.lazy_registry import build_lazy_toolkit
175
+ toolkit = build_lazy_toolkit(
176
+ active_categories=categories,
177
+ include_categories=categories + connector_services,
178
+ budget=int(os.environ.get("MEL_LAZY_BUDGET", "25")),
179
+ )
180
+ else:
181
+ toolkit = build_toolkit(categories=categories)
118
182
  except Exception as exc:
119
- _log(f"build_toolkit({categories}) failed: {exc}; retrying melaya_agent only")
183
+ _log(f"toolkit build failed (connectors={connector_services}): {exc}; retrying melaya_agent only")
120
184
  try:
121
185
  toolkit = build_toolkit(categories=["melaya_agent"])
122
186
  except Exception:
@@ -145,6 +209,14 @@ def _build_agent():
145
209
  "blocked; only then report what you did and what (if anything) is blocked.\n"
146
210
  if phone_enabled else ""
147
211
  )
212
+ connector_rule = (
213
+ "- You also have CONNECTOR tools the user enabled (" + ", ".join(connector_services) + "). "
214
+ "Their tools are not all loaded upfront — call search_tools(query=...) to find the "
215
+ "right one for the user's question, then activate_tool(name=...) to load it, then call "
216
+ "it. Prefer specific tools (e.g. odoo_sale_order_list) over generic ones. Never invent "
217
+ "data — read it from the connector.\n"
218
+ if connector_services else ""
219
+ )
148
220
  sys_prompt = (
149
221
  "You are the Melaya Assistant, the in-app copilot of the Melaya "
150
222
  "agent-orchestration platform. You have READ-ONLY tools over the data "
@@ -156,6 +228,7 @@ def _build_agent():
156
228
  "templates or evals — never invent numbers. For cost, melaya_cost_summary "
157
229
  "supports dimension='pipeline' to find which pipeline cost the most.\n"
158
230
  + phone_rule
231
+ + connector_rule
159
232
  + "- Be concise and concrete; format small tables or bullet lists when comparing items.\n"
160
233
  "- If a question is outside the platform, answer normally without tools.\n"
161
234
  + (f"- Answer in the user's language: {language}.\n" if language and language != "en" else "")
@@ -893,7 +893,13 @@ export async function connect(opts) {
893
893
  MEL_ASSISTANT_MODEL: String(payload.model || ""),
894
894
  MEL_ASSISTANT_LANGUAGE: String(payload.language || "en"),
895
895
  MEL_ASSISTANT_SURFACE: String(payload.surface || ""),
896
- // Per-user creds (incl. MELAYA_API_KEY for the tenant-gated tool bridge).
896
+ // Connector tool sets the user enabled for this chat (comma-joined service
897
+ // ids). The host seeds a lazy toolkit from these so ANY connector's tools
898
+ // are reachable without exploding context.
899
+ MEL_ASSISTANT_CONNECTORS: Array.isArray(payload.connectors) ? payload.connectors.join(",") : "",
900
+ // Gate write connector-tools behind the in-chat approval card (fail-safe).
901
+ MEL_ASSISTANT_CONNECTOR_HITL: Array.isArray(payload.connectors) && payload.connectors.length ? "1" : "",
902
+ // Per-user creds (incl. MELAYA_API_KEY + the selected connectors' env).
897
903
  ...(payload.credentials || {}),
898
904
  };
899
905
  const proc = spawn(envResult.pythonPath, ["-u", stagedHost], { cwd: workDir, env, stdio: ["pipe", "pipe", "pipe"] });
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "@melaya/runner",
3
- "version": "1.0.85",
4
- "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
- "license": "UNLICENSED",
6
- "private": false,
7
- "type": "module",
8
- "bin": {
9
- "melaya-runner": "dist/cli.js"
10
- },
11
- "main": "dist/index.js",
12
- "files": [
13
- "dist/**/*.js",
14
- "dist/**/*.d.ts",
15
- "dist/**/*.py",
16
- "localRagIngest.py",
17
- "localRagRetrieve.py",
18
- "nltk_data/**",
19
- "README.md"
20
- ],
21
- "scripts": {
22
- "build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py'); fs.copyFileSync('src/assistantHost.py','dist/assistantHost.py')\"",
23
- "prepublishOnly": "npm run build"
24
- },
25
- "dependencies": {
26
- "chalk": "^5.3.0",
27
- "commander": "^12.0.0",
28
- "ora": "^8.0.0",
29
- "playwright": "^1.47.0",
30
- "socket.io-client": "^4.8.0"
31
- },
32
- "devDependencies": {
33
- "@types/node": "^20.0.0",
34
- "typescript": "^5.5.0"
35
- },
36
- "engines": {
37
- "node": ">=18"
38
- },
39
- "publishConfig": {
40
- "access": "public"
41
- }
42
- }
1
+ {
2
+ "name": "@melaya/runner",
3
+ "version": "1.0.87",
4
+ "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "type": "module",
8
+ "bin": {
9
+ "melaya-runner": "dist/cli.js"
10
+ },
11
+ "main": "dist/index.js",
12
+ "files": [
13
+ "dist/**/*.js",
14
+ "dist/**/*.d.ts",
15
+ "dist/**/*.py",
16
+ "localRagIngest.py",
17
+ "localRagRetrieve.py",
18
+ "nltk_data/**",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py'); fs.copyFileSync('src/assistantHost.py','dist/assistantHost.py')\"",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "chalk": "^5.3.0",
27
+ "commander": "^12.0.0",
28
+ "ora": "^8.0.0",
29
+ "playwright": "^1.47.0",
30
+ "socket.io-client": "^4.8.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.0.0",
34
+ "typescript": "^5.5.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }