@ema.co/mcp-toolkit 2026.3.29-1 → 2026.4.9
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/dist/auth/login.js +1 -1
- package/dist/config/profile.js +1 -1
- package/dist/config/tool-guidance.js +5 -1
- package/dist/knowledge/extractors/openapi-endpoints.js +160 -2
- package/dist/knowledge/guidance-cache.js +4 -3
- package/dist/knowledge/search-client.js +44 -45
- package/dist/mcp/handlers/config/index.js +124 -8
- package/dist/mcp/handlers/knowledge/index.js +2 -1
- package/dist/mcp/knowledge-guidance-topics.js +25 -1
- package/dist/mcp/knowledge.js +1 -1
- package/dist/mcp/resources-dynamic.js +3 -3
- package/dist/mcp/tools.js +15 -3
- package/dist/sdk/generated/agent-catalog.js +8 -4
- package/dist/sdk/generated/deprecated-actions.js +19 -19
- package/dist/sdk/generated/proto-fields.js +1 -1
- package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +223 -15
- package/dist/sdk/generated/protos/service/common/v1/common_pb.js +51 -1
- package/dist/sdk/generated/protos/service/conversation_review/v1/conversation_review_pb.js +63 -16
- package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +65 -9
- package/dist/sdk/generated/protos/service/eval-tool/v1/evaluation_pb.js +114 -1
- package/dist/sdk/generated/protos/service/eval-tool/v1/testbed_pb.js +1 -1
- package/dist/sdk/generated/protos/service/external_access_service/v1/external_bot_pb.js +5 -1
- package/dist/sdk/generated/protos/service/external_tool_connection/v1/connection_manager_pb.js +114 -35
- package/dist/sdk/generated/protos/service/external_tool_connection/v1/ema_connector_pb.js +120 -0
- package/dist/sdk/generated/protos/service/identity/v1/identity_pb.js +188 -0
- package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +41 -2
- package/dist/sdk/generated/protos/service/permissions/permissions_pb.js +327 -24
- package/dist/sdk/generated/protos/service/persona/v1/chatbot_pb.js +41 -3
- package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +141 -89
- package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +283 -133
- package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +24 -3
- package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +114 -14
- package/dist/sdk/generated/protos/service/proposal/v1/proposal_pb.js +57 -1
- package/dist/sdk/generated/protos/service/script_executor/v1/script_executor_pb.js +96 -0
- package/dist/sdk/generated/protos/service/transform/v1/transform_pb.js +19 -1
- package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
- package/dist/sdk/generated/protos/service/voice/v1/external_voice_pb.js +132 -0
- package/dist/sdk/generated/protos/service/voice/v1/voice_pb.js +1 -1
- package/dist/sdk/generated/protos/service/webcrawl/v1/webcrawl_pb.js +7 -2
- package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +86 -49
- package/dist/sdk/generated/protos/service/workflows/v1/action_runner_pb.js +12 -7
- package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +27 -17
- package/dist/sdk/generated/protos/service/workflows/v1/agentic_search_pb.js +27 -0
- package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +30 -21
- package/dist/sdk/generated/protos/service/workflows/v1/dashboards_pb.js +35 -13
- package/dist/sdk/generated/protos/service/workflows/v1/embedded_persona_runner_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +19 -1
- package/dist/sdk/generated/protos/service/workflows/v1/log_type_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/rpc/workflow_rpc_pb.js +71 -14
- package/dist/sdk/generated/protos/service/workflows/v1/values_pb.js +8 -2
- package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +2 -1
- package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +31 -16
- package/dist/sdk/generated/template-fallbacks.js +1 -1
- package/dist/sdk/generated/well-known-types.js +4 -1
- package/package.json +1 -1
package/dist/auth/login.js
CHANGED
|
@@ -38,7 +38,7 @@ const WELL_KNOWN_APP_URLS = {
|
|
|
38
38
|
function resolveApiUrl(envName, explicit) {
|
|
39
39
|
if (explicit)
|
|
40
40
|
return explicit.replace(/\/$/, "");
|
|
41
|
-
return WELL_KNOWN_API_URLS[envName] ??
|
|
41
|
+
return WELL_KNOWN_API_URLS[envName] ?? `https://api.${envName}.ema.co`;
|
|
42
42
|
}
|
|
43
43
|
function resolveAppUrl(envName, explicit) {
|
|
44
44
|
if (explicit)
|
package/dist/config/profile.js
CHANGED
|
@@ -23,7 +23,7 @@ export function slugify(name) {
|
|
|
23
23
|
.replace(/^-|-$/g, "");
|
|
24
24
|
}
|
|
25
25
|
export function profileName(tenantSlug, env) {
|
|
26
|
-
return `${tenantSlug}-${env}`;
|
|
26
|
+
return `${tenantSlug}-${slugify(env)}`;
|
|
27
27
|
}
|
|
28
28
|
export function displayName(tenantName, env, store) {
|
|
29
29
|
const sameTenantCount = Object.values(store.profiles).filter((p) => p.tenant.name === tenantName).length;
|
|
@@ -21,6 +21,10 @@ export const TOOL_GUIDANCE = {
|
|
|
21
21
|
{ name: "Switch profile", description: "Switch active profile", example: 'config(method="use", profile="acme-corp-prod")' },
|
|
22
22
|
{ name: "List profiles", description: "List all profiles with auth status", example: 'config(method="profiles")' },
|
|
23
23
|
{ name: "Set default env", description: "Set default login environment", example: 'config(method="set", key="default_env", value="prod")' },
|
|
24
|
+
{ name: "Login (custom URL)", description: "Login to custom tenant environment (e.g. sub-tenant staging)", example: 'config(method="login", app_url="https://staging.acme.ema.co")' },
|
|
25
|
+
{ name: "Login (custom + API URL)", description: "Login with explicit API URL override", example: 'config(method="login", app_url="https://staging.acme.ema.co", api_url="https://api.staging.acme.ema.co")' },
|
|
26
|
+
{ name: "Set API URL", description: "Update current profile's API endpoint", example: 'config(method="set", key="api_url", value="https://api.custom.ema.co")' },
|
|
27
|
+
{ name: "Set app URL", description: "Update current profile's frontend URL", example: 'config(method="set", key="app_url", value="https://custom.ema.co")' },
|
|
24
28
|
],
|
|
25
29
|
nextSteps: {
|
|
26
30
|
status: "If auth is missing or expired, run login. If GCP auth is missing, run gcp_login. Both are required.",
|
|
@@ -115,7 +119,7 @@ export const TOOL_GUIDANCE = {
|
|
|
115
119
|
},
|
|
116
120
|
{
|
|
117
121
|
name: "Get AI answer",
|
|
118
|
-
description: "AI-generated summary with citations",
|
|
122
|
+
description: "AI-generated summary with citations. Response includes _warning about fabrication risk and _next_step for verification.",
|
|
119
123
|
example: 'knowledge("how to add HITL", detail="answer")',
|
|
120
124
|
},
|
|
121
125
|
{
|
|
@@ -1,3 +1,139 @@
|
|
|
1
|
+
const MAX_RESOLVE_DEPTH = 6;
|
|
2
|
+
/** Resolve a $ref string like "#/components/schemas/Foo" against the spec */
|
|
3
|
+
function resolveRef(ref, spec) {
|
|
4
|
+
const parts = ref.replace(/^#\//, "").split("/");
|
|
5
|
+
let current = spec;
|
|
6
|
+
for (const part of parts) {
|
|
7
|
+
if (typeof current !== "object" || !current)
|
|
8
|
+
return undefined;
|
|
9
|
+
current = current[part];
|
|
10
|
+
}
|
|
11
|
+
return current;
|
|
12
|
+
}
|
|
13
|
+
/** Get a human-readable type string for a schema node */
|
|
14
|
+
function schemaTypeString(node, spec, visited, depth) {
|
|
15
|
+
if (node.$ref) {
|
|
16
|
+
const refName = node.$ref.split("/").pop() ?? "object";
|
|
17
|
+
if (visited.has(node.$ref) || depth >= MAX_RESOLVE_DEPTH)
|
|
18
|
+
return refName;
|
|
19
|
+
const resolved = resolveRef(node.$ref, spec);
|
|
20
|
+
if (!resolved)
|
|
21
|
+
return refName;
|
|
22
|
+
return schemaTypeString(resolved, spec, new Set([...visited, node.$ref]), depth + 1);
|
|
23
|
+
}
|
|
24
|
+
if (node.enum)
|
|
25
|
+
return `enum(${node.enum.join("|")})`;
|
|
26
|
+
if (node.anyOf) {
|
|
27
|
+
const types = node.anyOf
|
|
28
|
+
.filter((s) => s.type !== "null")
|
|
29
|
+
.map((s) => schemaTypeString(s, spec, visited, depth + 1));
|
|
30
|
+
const hasNull = node.anyOf.some((s) => s.type === "null");
|
|
31
|
+
const base = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
|
|
32
|
+
return hasNull ? `${base} | null` : base;
|
|
33
|
+
}
|
|
34
|
+
if (node.allOf) {
|
|
35
|
+
return node.allOf.map((s) => schemaTypeString(s, spec, visited, depth + 1)).join(" & ");
|
|
36
|
+
}
|
|
37
|
+
if (node.type === "array" && node.items) {
|
|
38
|
+
return `${schemaTypeString(node.items, spec, visited, depth + 1)}[]`;
|
|
39
|
+
}
|
|
40
|
+
if (node.format)
|
|
41
|
+
return `${node.type ?? "string"}(${node.format})`;
|
|
42
|
+
return node.type ?? "object";
|
|
43
|
+
}
|
|
44
|
+
/** Resolve a schema to a flat list of fields (one level deep, with type strings) */
|
|
45
|
+
function resolveSchemaFields(schema, spec, visited = new Set(), depth = 0) {
|
|
46
|
+
if (depth >= MAX_RESOLVE_DEPTH)
|
|
47
|
+
return [];
|
|
48
|
+
// Follow $ref
|
|
49
|
+
if (schema.$ref) {
|
|
50
|
+
if (visited.has(schema.$ref))
|
|
51
|
+
return [{ name: "(circular)", type: schema.$ref.split("/").pop() ?? "object", required: false }];
|
|
52
|
+
const resolved = resolveRef(schema.$ref, spec);
|
|
53
|
+
if (!resolved)
|
|
54
|
+
return [];
|
|
55
|
+
return resolveSchemaFields(resolved, spec, new Set([...visited, schema.$ref]), depth + 1);
|
|
56
|
+
}
|
|
57
|
+
// Merge allOf — deduplicate by field name (last-wins, matching JSON Schema merge semantics)
|
|
58
|
+
if (schema.allOf) {
|
|
59
|
+
const merged = schema.allOf.flatMap((s) => resolveSchemaFields(s, spec, visited, depth + 1));
|
|
60
|
+
const seen = new Map();
|
|
61
|
+
for (const f of merged)
|
|
62
|
+
seen.set(f.name, f);
|
|
63
|
+
return [...seen.values()];
|
|
64
|
+
}
|
|
65
|
+
// Handle top-level anyOf (e.g., discriminator unions)
|
|
66
|
+
if (schema.anyOf) {
|
|
67
|
+
const nonNull = schema.anyOf.filter((s) => s.type !== "null");
|
|
68
|
+
return nonNull.flatMap((s) => resolveSchemaFields(s, spec, visited, depth + 1));
|
|
69
|
+
}
|
|
70
|
+
// Object with properties
|
|
71
|
+
if (schema.properties) {
|
|
72
|
+
const required = new Set(schema.required ?? []);
|
|
73
|
+
return Object.entries(schema.properties).map(([name, prop]) => ({
|
|
74
|
+
name,
|
|
75
|
+
type: schemaTypeString(prop, spec, visited, depth + 1),
|
|
76
|
+
required: required.has(name),
|
|
77
|
+
description: prop.description,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
/** Format resolved fields as a readable markdown-style block */
|
|
83
|
+
function formatFields(fields) {
|
|
84
|
+
if (fields.length === 0)
|
|
85
|
+
return "";
|
|
86
|
+
return fields
|
|
87
|
+
.map((f) => {
|
|
88
|
+
const req = f.required ? " (required)" : "";
|
|
89
|
+
const desc = f.description ? ` — ${f.description}` : "";
|
|
90
|
+
return ` - ${f.name}: ${f.type}${req}${desc}`;
|
|
91
|
+
})
|
|
92
|
+
.join("\n");
|
|
93
|
+
}
|
|
94
|
+
function buildRequestBodySection(op, spec) {
|
|
95
|
+
if (!op.requestBody?.content)
|
|
96
|
+
return "";
|
|
97
|
+
const lines = ["", "Request Body:"];
|
|
98
|
+
if (op.requestBody.description)
|
|
99
|
+
lines.push(` ${op.requestBody.description}`);
|
|
100
|
+
if (op.requestBody.required)
|
|
101
|
+
lines.push(" (required)");
|
|
102
|
+
for (const [contentType, media] of Object.entries(op.requestBody.content)) {
|
|
103
|
+
lines.push(` Content-Type: ${contentType}`);
|
|
104
|
+
if (media.schema) {
|
|
105
|
+
const fields = resolveSchemaFields(media.schema, spec);
|
|
106
|
+
if (fields.length > 0) {
|
|
107
|
+
lines.push(" Fields:");
|
|
108
|
+
lines.push(formatFields(fields));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function buildResponsesSection(op, spec) {
|
|
115
|
+
if (!op.responses)
|
|
116
|
+
return "";
|
|
117
|
+
const lines = ["", "Responses:"];
|
|
118
|
+
for (const [statusCode, response] of Object.entries(op.responses)) {
|
|
119
|
+
lines.push(` ${statusCode}: ${response.description ?? ""}`);
|
|
120
|
+
if (response.content) {
|
|
121
|
+
for (const [contentType, media] of Object.entries(response.content)) {
|
|
122
|
+
if (media.schema) {
|
|
123
|
+
const fields = resolveSchemaFields(media.schema, spec);
|
|
124
|
+
if (fields.length > 0) {
|
|
125
|
+
lines.push(` ${contentType}:`);
|
|
126
|
+
lines.push(formatFields(fields));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
// Main extractor
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1
137
|
export async function extractOpenAPIEndpoints(config) {
|
|
2
138
|
const specPath = config.from ?? "src/sdk/generated/openapi.json";
|
|
3
139
|
const { createRequire } = await import("node:module");
|
|
@@ -7,22 +143,39 @@ export async function extractOpenAPIEndpoints(config) {
|
|
|
7
143
|
const fullPath = resolve(process.cwd(), specPath);
|
|
8
144
|
const spec = require(fullPath);
|
|
9
145
|
const docs = [];
|
|
10
|
-
const paths = spec.paths ?? {};
|
|
146
|
+
const paths = (spec.paths ?? {});
|
|
147
|
+
let schemasResolved = 0;
|
|
148
|
+
const HTTP_METHODS = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
|
|
11
149
|
for (const [path, methods] of Object.entries(paths)) {
|
|
12
150
|
for (const [method, op] of Object.entries(methods)) {
|
|
151
|
+
if (!HTTP_METHODS.has(method))
|
|
152
|
+
continue;
|
|
13
153
|
if (typeof op !== "object" || !op)
|
|
14
154
|
continue;
|
|
15
155
|
const operation = op;
|
|
16
156
|
const opId = operation.operationId ?? `${method}_${path.replace(/\//g, "_")}`;
|
|
157
|
+
// Build rich content with resolved schemas
|
|
17
158
|
const content = [
|
|
18
159
|
`${method.toUpperCase()} ${path}`,
|
|
19
160
|
operation.summary ?? "",
|
|
20
161
|
operation.description ?? "",
|
|
21
162
|
operation.tags?.length ? `Tags: ${operation.tags.join(", ")}` : "",
|
|
163
|
+
// Parameters with descriptions
|
|
22
164
|
operation.parameters?.length
|
|
23
|
-
? `Parameters
|
|
165
|
+
? `Parameters:\n${operation.parameters.map((p) => {
|
|
166
|
+
const typeStr = p.schema ? schemaTypeString(p.schema, spec, new Set(), 0) : "";
|
|
167
|
+
const desc = p.description ? ` — ${p.description}` : "";
|
|
168
|
+
return ` - ${p.name} (${p.in}${p.required ? ", required" : ""}${typeStr ? `: ${typeStr}` : ""})${desc}`;
|
|
169
|
+
}).join("\n")}`
|
|
24
170
|
: "",
|
|
171
|
+
// Request body with resolved schema fields
|
|
172
|
+
buildRequestBodySection(operation, spec),
|
|
173
|
+
// Response bodies with resolved schema fields
|
|
174
|
+
buildResponsesSection(operation, spec),
|
|
25
175
|
].filter(Boolean).join("\n");
|
|
176
|
+
// Track schema resolution metrics
|
|
177
|
+
if (operation.requestBody?.content || operation.responses)
|
|
178
|
+
schemasResolved++;
|
|
26
179
|
docs.push({
|
|
27
180
|
id: `endpoint:${opId}`,
|
|
28
181
|
structData: {
|
|
@@ -49,10 +202,15 @@ export async function extractOpenAPIEndpoints(config) {
|
|
|
49
202
|
source_path: specPath,
|
|
50
203
|
http_method: method.toUpperCase(),
|
|
51
204
|
http_path: path,
|
|
205
|
+
protocol: "rest",
|
|
206
|
+
visibility: "external",
|
|
207
|
+
has_request_schema: !!operation.requestBody?.content,
|
|
208
|
+
has_response_schema: !!operation.responses,
|
|
52
209
|
},
|
|
53
210
|
});
|
|
54
211
|
}
|
|
55
212
|
}
|
|
213
|
+
process.stderr.write(` OpenAPI: ${docs.length} endpoints, ${schemasResolved} with resolved schemas\n`);
|
|
56
214
|
return { documents: docs, edges: [] };
|
|
57
215
|
}
|
|
58
216
|
catch (err) {
|
|
@@ -212,7 +212,8 @@ export class GuidanceCache {
|
|
|
212
212
|
case "structural-invariant":
|
|
213
213
|
this.invariants.push(data);
|
|
214
214
|
break;
|
|
215
|
-
case "contextual-guidance":
|
|
215
|
+
case "contextual-guidance":
|
|
216
|
+
case "guide": {
|
|
216
217
|
const atom = data;
|
|
217
218
|
const key = atom.key
|
|
218
219
|
?? `${atom.tool ?? "*"}.${atom.method ?? "*"}.${atom.result_shape ?? "*"}`;
|
|
@@ -244,10 +245,10 @@ export class GuidanceCache {
|
|
|
244
245
|
return "qualifying-question";
|
|
245
246
|
if (tags.includes("structural-invariant"))
|
|
246
247
|
return "structural-invariant";
|
|
247
|
-
if (tags.includes("structural-invariant"))
|
|
248
|
-
return "structural-invariant";
|
|
249
248
|
if (tags.includes("contextual-guidance"))
|
|
250
249
|
return "contextual-guidance";
|
|
250
|
+
if (tags.includes("guide"))
|
|
251
|
+
return "guide";
|
|
251
252
|
if (name.includes("tool-guidance"))
|
|
252
253
|
return "tool-guidance";
|
|
253
254
|
if (name.startsWith("invariant-"))
|
|
@@ -29,9 +29,11 @@ import { getActiveScopes } from "./scopes.js";
|
|
|
29
29
|
*/
|
|
30
30
|
const PROMOTED_FIELDS = new Set([
|
|
31
31
|
// Promoted to top-level fields in search results
|
|
32
|
-
"id", "name", "
|
|
33
|
-
"source", "
|
|
34
|
-
|
|
32
|
+
"id", "name", "description", "summary", "type", "doc_type", "scope", "scopes",
|
|
33
|
+
"source", "sources", "status",
|
|
34
|
+
// Legacy v2 fields (still on ~829 MCP-extracted docs)
|
|
35
|
+
"confidence", "confidence_score", "provenance", "tags",
|
|
36
|
+
"tier", "severity", "category", "when_to_use",
|
|
35
37
|
"domain", "audience", "source_repo",
|
|
36
38
|
// Heavy text — already in content.rawBytes, not needed in results
|
|
37
39
|
"content",
|
|
@@ -121,26 +123,29 @@ export async function ensureGcpAuth() {
|
|
|
121
123
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
124
|
// Filter building — dynamic scope filters (DE static controls can't vary per-query)
|
|
123
125
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
|
-
|
|
126
|
+
// v3 structData: name, description, scopes (array), source, sources, status, signals.
|
|
127
|
+
// scopes for access filtering, source for provenance, status for lifecycle.
|
|
128
|
+
const FILTERABLE_KEYS = new Set(["scopes", "source", "status"]);
|
|
125
129
|
function buildFilterExpression(filters, elevate) {
|
|
126
130
|
const parts = [];
|
|
127
|
-
// Scope filter
|
|
128
|
-
// elevate=true removes
|
|
129
|
-
// Callers can pass explicit scope filters to override.
|
|
131
|
+
// Scope filter on scopes (array field). DE ANY() matches array elements.
|
|
132
|
+
// elevate=true removes scope filter (admin access).
|
|
130
133
|
const hasExplicitScope = filters?.scope && filters.scope.length > 0;
|
|
131
134
|
if (!elevate && !hasExplicitScope) {
|
|
132
135
|
const scopes = getActiveScopes();
|
|
133
136
|
const quoted = scopes.map((s) => `"${s}"`).join(",");
|
|
134
|
-
parts.push(`
|
|
137
|
+
parts.push(`scopes: ANY(${quoted})`);
|
|
135
138
|
}
|
|
136
139
|
if (filters) {
|
|
137
140
|
for (const [key, values] of Object.entries(filters)) {
|
|
138
141
|
if (!values || values.length === 0)
|
|
139
142
|
continue;
|
|
140
|
-
|
|
143
|
+
// Map caller's "scope" to v3's "scopes" field name
|
|
144
|
+
const deField = key === "scope" ? "scopes" : key;
|
|
145
|
+
if (!FILTERABLE_KEYS.has(deField))
|
|
141
146
|
continue;
|
|
142
147
|
const quoted = values.map((v) => `"${v}"`).join(",");
|
|
143
|
-
parts.push(`${
|
|
148
|
+
parts.push(`${deField}: ANY(${quoted})`);
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
return parts.join(" AND ");
|
|
@@ -168,13 +173,27 @@ const QUERY_SIGNALS = [
|
|
|
168
173
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
174
|
// Dynamic Preamble — context-aware system instructions for :answer endpoint
|
|
170
175
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Base preamble applied to all answer queries.
|
|
178
|
+
*
|
|
179
|
+
* The Ema-specific context line applies to the primary use case (Ema platform knowledge).
|
|
180
|
+
* The code-accuracy rules are domain-agnostic and apply to ANY knowledge corpus that
|
|
181
|
+
* contains code examples, API formats, or structured data — they prevent the generative
|
|
182
|
+
* model from fabricating structures that look plausible but are wrong.
|
|
183
|
+
*/
|
|
172
184
|
const BASE_PREAMBLE = [
|
|
185
|
+
// Domain context (Ema-specific — other corpora would swap this line)
|
|
173
186
|
"You are answering questions about the Ema AI Employee platform — a workflow orchestration system.",
|
|
174
|
-
"When referencing actions or nodes, use their exact
|
|
175
|
-
|
|
187
|
+
"When referencing actions or nodes, use their exact names (e.g., chat_categorizer, respond_with_sources).",
|
|
188
|
+
// Code accuracy (domain-agnostic — applies to any technical corpus)
|
|
189
|
+
"CRITICAL — code accuracy rules:",
|
|
190
|
+
"1. NEVER assemble, fabricate, or synthesize complete code examples (JSON, YAML, config). The risk of structural errors is too high.",
|
|
191
|
+
"2. For code formats: REFERENCE the source document by name/ID and tell the user to fetch it directly. Example: 'See schema/workflow-def for the complete deployable example.'",
|
|
192
|
+
"3. You MAY quote SHORT code fragments (under 5 lines) verbatim from source documents with attribution.",
|
|
193
|
+
"4. You MAY describe the structure in words (e.g., 'actions is an array of objects, each with name, action, and inputs').",
|
|
194
|
+
"5. For canonical formats, direct users to the authoritative source rather than reproducing it.",
|
|
176
195
|
"Cite specific document IDs when available.",
|
|
177
|
-
].join("
|
|
196
|
+
].join("\n");
|
|
178
197
|
/** Query-pattern-specific preamble additions. */
|
|
179
198
|
const PREAMBLE_SIGNALS = [
|
|
180
199
|
{
|
|
@@ -183,7 +202,7 @@ const PREAMBLE_SIGNALS = [
|
|
|
183
202
|
},
|
|
184
203
|
{
|
|
185
204
|
pattern: /\b(how to|how do|pattern|example|wire|connect|build)\b/i,
|
|
186
|
-
addition: "Provide step-by-step instructions
|
|
205
|
+
addition: "Provide step-by-step instructions. For complete code examples, reference the source document (e.g., 'see schema/workflow-def for the deployable example') rather than reproducing it. The agent will fetch the document directly.",
|
|
187
206
|
},
|
|
188
207
|
{
|
|
189
208
|
pattern: /\b(rule|constraint|invariant|must|require|validate)\b/i,
|
|
@@ -246,25 +265,15 @@ function transformDeResponse(deResponse, mode) {
|
|
|
246
265
|
document: {
|
|
247
266
|
id: (structData.id ?? doc.id ?? ""),
|
|
248
267
|
name: (structData.name ?? ""),
|
|
268
|
+
// v3: description is the summary. Fall back to v2 fields for legacy docs.
|
|
249
269
|
type: (structData.type ?? structData.doc_type ?? ""),
|
|
250
270
|
scope: (structData.scope ?? "public"),
|
|
251
271
|
source: (structData.source ?? ""),
|
|
252
|
-
summary: (structData.summary ?? ""),
|
|
253
|
-
confidence_score: (structData.confidence_score ?? 0),
|
|
272
|
+
summary: (structData.description ?? structData.summary ?? ""),
|
|
254
273
|
status: (structData.status ?? "active"),
|
|
255
|
-
//
|
|
256
|
-
...(structData.
|
|
257
|
-
...(structData.
|
|
258
|
-
...(structData.category ? { category: structData.category } : {}),
|
|
259
|
-
...(structData.when_to_use ? { when_to_use: structData.when_to_use } : {}),
|
|
260
|
-
...(structData.tier ? { tier: structData.tier } : {}),
|
|
261
|
-
...(structData.severity ? { severity: structData.severity } : {}),
|
|
262
|
-
...(structData.provenance ? { provenance: structData.provenance } : {}),
|
|
263
|
-
...(Array.isArray(structData.tags) ? { tags: structData.tags } : {}),
|
|
264
|
-
// Domain isolation + audience targeting
|
|
265
|
-
...(structData.domain ? { domain: structData.domain } : {}),
|
|
266
|
-
...(Array.isArray(structData.audience) ? { audience: structData.audience } : {}),
|
|
267
|
-
...(structData.source_repo ? { source_repo: structData.source_repo } : {}),
|
|
274
|
+
// v3 fields
|
|
275
|
+
...(Array.isArray(structData.scopes) ? { scopes: structData.scopes } : {}),
|
|
276
|
+
...(Array.isArray(structData.sources) ? { sources: structData.sources } : {}),
|
|
268
277
|
// Non-promoted metadata only (no duplication)
|
|
269
278
|
structData: stripPromotedFields(structData),
|
|
270
279
|
},
|
|
@@ -478,22 +487,12 @@ async function searchDirect(query, options) {
|
|
|
478
487
|
// Dynamic domain boost — if query signals a specific platform, boost its domain
|
|
479
488
|
// and demote the other. DE serves both platforms; this keeps results focused.
|
|
480
489
|
const queryBoost = buildQueryBoostSpec(query, filters);
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// regardless of relevance. DE boost values are additive to relevance score.
|
|
484
|
-
// Values calibrated against signal viewer: semantic relevance spreads 0.07-0.99,
|
|
485
|
-
// so boosts must be large enough to move docs across that range.
|
|
486
|
-
const confidenceBoosts = [
|
|
487
|
-
{ condition: 'confidence: ANY("verified")', boost: 0.5 },
|
|
488
|
-
{ condition: 'confidence: ANY("inferred")', boost: -0.2 },
|
|
489
|
-
{ condition: 'confidence: ANY("low-confidence")', boost: -0.8 },
|
|
490
|
-
];
|
|
490
|
+
// v3: confidence removed from structData. DE text matching handles relevance.
|
|
491
|
+
// Query-specific boosts (domain targeting) still apply.
|
|
491
492
|
const querySpecs = (queryBoost?.conditionBoostSpecs ?? []);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
];
|
|
496
|
-
body.boostSpec = { conditionBoostSpecs: allBoosts };
|
|
493
|
+
if (querySpecs.length > 0) {
|
|
494
|
+
body.boostSpec = { conditionBoostSpecs: querySpecs };
|
|
495
|
+
}
|
|
497
496
|
// Always request snippets — works with chunked datastores.
|
|
498
497
|
// (Extractive answers do NOT work with chunking, only snippets.)
|
|
499
498
|
// For answer mode, also request summary with citations.
|
|
@@ -7,6 +7,57 @@
|
|
|
7
7
|
import { loadConfig, saveConfig, addProfile, removeProfile, setCurrentProfile, listProfiles, getActiveProfile, slugify, profileName, } from "../../../config/profile.js";
|
|
8
8
|
import { invalidateEnvCache } from "../env/config.js";
|
|
9
9
|
import { tryGcloudAuth } from "../../../auth/gcloud.js";
|
|
10
|
+
/**
|
|
11
|
+
* Derive API URL, app URL, and environment name from a custom app URL.
|
|
12
|
+
* Supports any `*.ema.co` hostname. Returns null for non-ema.co domains.
|
|
13
|
+
*/
|
|
14
|
+
function deriveFromAppUrl(appUrl) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(appUrl.includes("://") ? appUrl : `https://${appUrl}`);
|
|
17
|
+
const hostname = url.hostname;
|
|
18
|
+
// Match *.ema.co pattern
|
|
19
|
+
const match = hostname.match(/^(.+)\.ema\.co$/);
|
|
20
|
+
if (!match)
|
|
21
|
+
return null;
|
|
22
|
+
const subdomain = match[1]; // e.g., "staging.acme", "app", "demo"
|
|
23
|
+
// Well-known frontends map to standard envs
|
|
24
|
+
if (subdomain === "app") {
|
|
25
|
+
return { apiUrl: "https://api.ema.co", appUrl: "https://app.ema.co", envName: "prod" };
|
|
26
|
+
}
|
|
27
|
+
if (["demo", "staging", "dev"].includes(subdomain)) {
|
|
28
|
+
return {
|
|
29
|
+
apiUrl: `https://api.${subdomain}.ema.co`,
|
|
30
|
+
appUrl: `https://${subdomain}.ema.co`,
|
|
31
|
+
envName: subdomain,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Custom subdomain: derive API URL by prepending "api."
|
|
35
|
+
return {
|
|
36
|
+
apiUrl: `https://api.${subdomain}.ema.co`,
|
|
37
|
+
appUrl: `https://${subdomain}.ema.co`,
|
|
38
|
+
envName: subdomain,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Probe an API URL to check reachability. Returns true if the endpoint
|
|
47
|
+
* responds (even with 401/403 — means it's reachable but auth-gated).
|
|
48
|
+
*/
|
|
49
|
+
async function probeApiUrl(apiUrl) {
|
|
50
|
+
try {
|
|
51
|
+
const resp = await fetch(`${apiUrl.replace(/\/$/, "")}/api/health`, {
|
|
52
|
+
method: "HEAD",
|
|
53
|
+
signal: AbortSignal.timeout(5000),
|
|
54
|
+
});
|
|
55
|
+
return resp.ok || resp.status === 401 || resp.status === 403;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
10
61
|
export async function handleConfig(args) {
|
|
11
62
|
const method = args.method;
|
|
12
63
|
switch (method) {
|
|
@@ -161,14 +212,56 @@ async function handleStatus() {
|
|
|
161
212
|
// Login
|
|
162
213
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
214
|
async function handleLogin(args) {
|
|
164
|
-
|
|
165
|
-
const
|
|
215
|
+
const customAppUrl = args.app_url;
|
|
216
|
+
const customApiUrl = args.api_url;
|
|
166
217
|
const tenantHint = args.tenant;
|
|
167
218
|
const directToken = args.token;
|
|
219
|
+
// Resolve URLs and env name from custom app_url or standard env
|
|
220
|
+
let envName;
|
|
221
|
+
let resolvedApiUrl;
|
|
222
|
+
let resolvedAppUrl;
|
|
223
|
+
let probeWarning;
|
|
224
|
+
if (customAppUrl) {
|
|
225
|
+
const derived = deriveFromAppUrl(customAppUrl);
|
|
226
|
+
if (derived) {
|
|
227
|
+
envName = args.env || derived.envName;
|
|
228
|
+
resolvedApiUrl = customApiUrl?.replace(/\/$/, "") ?? derived.apiUrl;
|
|
229
|
+
resolvedAppUrl = derived.appUrl;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Non-ema.co domain — derive env from hostname, require api_url if not provided
|
|
233
|
+
const parsed = new URL(customAppUrl.includes("://") ? customAppUrl : `https://${customAppUrl}`);
|
|
234
|
+
envName = args.env || slugify(parsed.hostname.split(".")[0]);
|
|
235
|
+
resolvedAppUrl = `${parsed.protocol}//${parsed.host}`;
|
|
236
|
+
if (customApiUrl) {
|
|
237
|
+
resolvedApiUrl = customApiUrl.replace(/\/$/, "");
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Best guess: api.{hostname}
|
|
241
|
+
resolvedApiUrl = `${parsed.protocol}//api.${parsed.host}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Probe the API URL — warn but don't block if unreachable
|
|
245
|
+
const reachable = await probeApiUrl(resolvedApiUrl);
|
|
246
|
+
if (!reachable) {
|
|
247
|
+
probeWarning =
|
|
248
|
+
`Derived API URL (${resolvedApiUrl}) did not respond. ` +
|
|
249
|
+
`Login will proceed with this URL, but if auth fails, verify the correct API URL ` +
|
|
250
|
+
`and retry with: config(method="login", app_url="${customAppUrl}", api_url="<correct_url>")`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Standard flow: explicit env > preference default_env > "prod"
|
|
255
|
+
envName = args.env || loadConfig().preferences.default_env || "prod";
|
|
256
|
+
resolvedApiUrl = customApiUrl?.replace(/\/$/, "")
|
|
257
|
+
?? (envName === "prod" ? "https://api.ema.co" : `https://api.${envName}.ema.co`);
|
|
258
|
+
}
|
|
168
259
|
try {
|
|
169
260
|
const { login } = await import("../../../auth/login.js");
|
|
170
261
|
const result = await login({
|
|
171
262
|
environment: envName,
|
|
263
|
+
baseUrl: resolvedApiUrl,
|
|
264
|
+
...(resolvedAppUrl ? { appUrl: resolvedAppUrl } : {}),
|
|
172
265
|
tenantId: tenantHint,
|
|
173
266
|
token: directToken,
|
|
174
267
|
});
|
|
@@ -183,7 +276,8 @@ async function handleLogin(args) {
|
|
|
183
276
|
},
|
|
184
277
|
environment: {
|
|
185
278
|
name: envName,
|
|
186
|
-
url:
|
|
279
|
+
url: resolvedApiUrl,
|
|
280
|
+
...(resolvedAppUrl ? { app_url: resolvedAppUrl } : {}),
|
|
187
281
|
},
|
|
188
282
|
user: { email: result.userEmail ?? "" },
|
|
189
283
|
created_at: new Date().toISOString(),
|
|
@@ -225,6 +319,7 @@ async function handleLogin(args) {
|
|
|
225
319
|
profile_name: profileName(slugify(t.company_name), envName),
|
|
226
320
|
active: t.tenant_id === result.tenantId,
|
|
227
321
|
})),
|
|
322
|
+
...(probeWarning ? { _warning: probeWarning } : {}),
|
|
228
323
|
_tip: "Token stored. All tools will now use this profile automatically.",
|
|
229
324
|
_next_step: `persona(method="list", profile="${name}")`,
|
|
230
325
|
};
|
|
@@ -362,7 +457,7 @@ async function handleUse(args) {
|
|
|
362
457
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
363
458
|
// Set / Get preferences
|
|
364
459
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
365
|
-
const ALLOWED_KEYS = ["debug", "default_env", "resource_source"];
|
|
460
|
+
const ALLOWED_KEYS = ["debug", "default_env", "resource_source", "api_url", "app_url"];
|
|
366
461
|
async function handleSet(args) {
|
|
367
462
|
const key = args.key;
|
|
368
463
|
const value = args.value;
|
|
@@ -372,11 +467,14 @@ async function handleSet(args) {
|
|
|
372
467
|
error: `Unknown preference key: ${key}. Available: ${ALLOWED_KEYS.join(", ")}`,
|
|
373
468
|
};
|
|
374
469
|
}
|
|
375
|
-
// Validate default_env values
|
|
470
|
+
// Validate default_env values — accept well-known envs + any env from existing profiles
|
|
376
471
|
if (key === "default_env") {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
472
|
+
const config = loadConfig();
|
|
473
|
+
const wellKnown = ["prod", "staging", "dev", "demo"];
|
|
474
|
+
const profileEnvNames = Object.values(config.profiles).map(p => p.environment.name);
|
|
475
|
+
const allValid = [...new Set([...wellKnown, ...profileEnvNames])];
|
|
476
|
+
if (!allValid.includes(value)) {
|
|
477
|
+
return { error: `Invalid default_env: ${value}. Must be one of: ${allValid.join(", ")}` };
|
|
380
478
|
}
|
|
381
479
|
}
|
|
382
480
|
// Validate resource_source values
|
|
@@ -386,6 +484,24 @@ async function handleSet(args) {
|
|
|
386
484
|
return { error: `Invalid resource_source: ${value}. Must be one of: ${valid.join(", ")}` };
|
|
387
485
|
}
|
|
388
486
|
}
|
|
487
|
+
// api_url and app_url update the current profile's environment URLs
|
|
488
|
+
if (key === "api_url" || key === "app_url") {
|
|
489
|
+
const config = loadConfig();
|
|
490
|
+
const profile = config.profiles[config.current_profile];
|
|
491
|
+
if (!profile) {
|
|
492
|
+
return { error: "No active profile. Log in first.", _tip: 'config(method="login")' };
|
|
493
|
+
}
|
|
494
|
+
const previous = key === "api_url" ? profile.environment.url : profile.environment.app_url;
|
|
495
|
+
if (key === "api_url") {
|
|
496
|
+
profile.environment.url = value;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
profile.environment.app_url = value;
|
|
500
|
+
}
|
|
501
|
+
saveConfig(config);
|
|
502
|
+
invalidateEnvCache();
|
|
503
|
+
return { set: { key, value, previous, profile: config.current_profile } };
|
|
504
|
+
}
|
|
389
505
|
const config = loadConfig();
|
|
390
506
|
const previous = config.preferences[key];
|
|
391
507
|
config.preferences[key] = value;
|
|
@@ -570,7 +570,8 @@ async function handleQuery(args) {
|
|
|
570
570
|
if (response.generativeAnswer) {
|
|
571
571
|
result.generative_answer = response.generativeAnswer;
|
|
572
572
|
result.citations = response.citations;
|
|
573
|
-
result._warning = "Generative answer is AI-synthesized.
|
|
573
|
+
result._warning = "Generative answer is AI-synthesized. Code examples may be fabricated or simplified — NEVER copy JSON without verification.";
|
|
574
|
+
result._next_step = 'Use workflow(mode="get") for canonical workflow_def format, or knowledge("schema/workflow-def", detail="excerpts") for validated examples.';
|
|
574
575
|
}
|
|
575
576
|
// Follow-up token for multi-turn answer conversations
|
|
576
577
|
if (response.answerQueryToken) {
|