@ema.co/mcp-toolkit 2026.3.29-1 → 2026.4.9-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/dist/auth/credentials.js +3 -0
- package/dist/auth/login.js +1 -1
- package/dist/config/profile.js +16 -1
- package/dist/config/tool-guidance.js +8 -2
- package/dist/knowledge/extractors/openapi-endpoints.js +160 -2
- package/dist/knowledge/guidance-cache.js +4 -3
- package/dist/knowledge/pipeline/confidence.js +13 -0
- package/dist/knowledge/pipeline/document.js +31 -2
- package/dist/knowledge/search-client.js +58 -44
- package/dist/mcp/guidance.js +1 -1
- package/dist/mcp/handlers/config/index.js +197 -11
- package/dist/mcp/handlers/feedback/index.js +9 -5
- package/dist/mcp/handlers/feedback/store.js +23 -9
- package/dist/mcp/handlers/knowledge/index.js +147 -113
- package/dist/mcp/handlers/response-actions.js +10 -11
- package/dist/mcp/knowledge-guidance-topics.js +40 -5
- package/dist/mcp/knowledge.js +1 -1
- package/dist/mcp/resources-dynamic.js +3 -3
- package/dist/mcp/server.js +4 -0
- package/dist/mcp/tools.js +23 -7
- 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/credentials.js
CHANGED
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;
|
|
@@ -227,6 +227,18 @@ export function resolveProfile(nameOrAlias, dir) {
|
|
|
227
227
|
// 3. No match in config.json
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
|
+
/** Smart color default based on environment name. */
|
|
231
|
+
export function defaultColorForEnv(envName) {
|
|
232
|
+
if (envName === "prod")
|
|
233
|
+
return "red";
|
|
234
|
+
if (envName === "staging")
|
|
235
|
+
return "yellow";
|
|
236
|
+
if (envName === "dev")
|
|
237
|
+
return "cyan";
|
|
238
|
+
if (envName === "demo")
|
|
239
|
+
return "green";
|
|
240
|
+
return "blue";
|
|
241
|
+
}
|
|
230
242
|
/**
|
|
231
243
|
* Get the active profile with display info and auth status.
|
|
232
244
|
* Returns null if no config or no current profile is set.
|
|
@@ -299,6 +311,9 @@ export function getActiveProfile(dir) {
|
|
|
299
311
|
tenant: profile.tenant.name,
|
|
300
312
|
tenant_id: profile.tenant.id,
|
|
301
313
|
env: profile.environment.name,
|
|
314
|
+
base_url: profile.environment.url,
|
|
315
|
+
...(profile.environment.app_url ? { app_url: profile.environment.app_url } : {}),
|
|
316
|
+
color: profile.color ?? defaultColorForEnv(profile.environment.name),
|
|
302
317
|
knowledge_scopes: knowledgeScopes,
|
|
303
318
|
auth_expires_in: authExpiresIn,
|
|
304
319
|
knowledge_search_available: searchAvailable,
|
|
@@ -15,12 +15,18 @@ export const TOOL_GUIDANCE = {
|
|
|
15
15
|
operations: [
|
|
16
16
|
{ name: "Status", description: "Show current profile, auth status, GCP auth", example: "config()" },
|
|
17
17
|
{ name: "Login", description: "Authenticate via browser OAuth", example: 'config(method="login")' },
|
|
18
|
-
{ name: "Login (token)", description: "Agent-mediated login —
|
|
18
|
+
{ name: "Login (token)", description: "Agent-mediated login — env auto-detected from JWT tenant_id or issuer", example: 'config(method="login", token="eyJ...")' },
|
|
19
19
|
{ name: "GCP login", description: "Standalone Google auth for knowledge search", example: 'config(method="gcp_login")' },
|
|
20
20
|
{ name: "GCP login (token)", description: "Agent-mediated GCP auth — user pastes token in chat", example: 'config(method="gcp_login", gcp_token="ya29...")' },
|
|
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")' },
|
|
28
|
+
{ name: "Login (non-ema.co)", description: "Custom domain login (api_url required for non-ema.co)", example: 'config(method="login", app_url="https://ai.acme.com", api_url="https://api.acme.com")' },
|
|
29
|
+
{ name: "Set profile color", description: "Set visual color for active profile (agent applies to IDE)", example: 'config(method="set", key="color", value="red")' },
|
|
24
30
|
],
|
|
25
31
|
nextSteps: {
|
|
26
32
|
status: "If auth is missing or expired, run login. If GCP auth is missing, run gcp_login. Both are required.",
|
|
@@ -115,7 +121,7 @@ export const TOOL_GUIDANCE = {
|
|
|
115
121
|
},
|
|
116
122
|
{
|
|
117
123
|
name: "Get AI answer",
|
|
118
|
-
description: "AI-generated summary with citations",
|
|
124
|
+
description: "AI-generated summary with citations. Response includes _warning about fabrication risk and _next_step for verification.",
|
|
119
125
|
example: 'knowledge("how to add HITL", detail="answer")',
|
|
120
126
|
},
|
|
121
127
|
{
|
|
@@ -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-"))
|
|
@@ -85,6 +85,19 @@ export function scoreToLabel(score) {
|
|
|
85
85
|
return "inferred";
|
|
86
86
|
return "low-confidence";
|
|
87
87
|
}
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Fragment helpers — GitHub-style #fragment on knowledge_ref
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
/** Strip #fragment from a knowledge_ref, returning the bare document ID. */
|
|
92
|
+
export function stripFragment(ref) {
|
|
93
|
+
const idx = ref.indexOf("#");
|
|
94
|
+
return idx === -1 ? ref : ref.slice(0, idx);
|
|
95
|
+
}
|
|
96
|
+
/** Extract the #fragment from a knowledge_ref, or undefined if none. */
|
|
97
|
+
export function extractFragment(ref) {
|
|
98
|
+
const idx = ref.indexOf("#");
|
|
99
|
+
return idx === -1 ? undefined : ref.slice(idx + 1);
|
|
100
|
+
}
|
|
88
101
|
/**
|
|
89
102
|
* Compute confidence adjustment based on the ratio of negative to total feedback.
|
|
90
103
|
*
|
|
@@ -34,20 +34,30 @@ export function toDeDocument(doc) {
|
|
|
34
34
|
const textContent = doc.structData?.content
|
|
35
35
|
|| [doc.structData?.name, doc.structData?.summary, doc.structData?.description].filter(Boolean).join("\n");
|
|
36
36
|
const contentHash = computeContentHash(textContent || doc.id);
|
|
37
|
+
const now = new Date().toISOString();
|
|
37
38
|
const augmented = {
|
|
38
39
|
...doc.structData,
|
|
39
40
|
content_hash: contentHash,
|
|
40
|
-
augmented_at:
|
|
41
|
+
augmented_at: now,
|
|
41
42
|
status: doc.structData?.status ?? "active",
|
|
42
43
|
confidence_score: doc.structData?.confidence_score
|
|
43
44
|
?? computeConfidenceScore(doc.structData?.provenance ?? "inferred"),
|
|
45
|
+
// Lifecycle timestamps — set once at publish, preserved on re-publish
|
|
46
|
+
// Aligned with knowledge-service's 3-layer model (produced_at/published_at)
|
|
47
|
+
published_at: doc.structData?.published_at ?? now,
|
|
48
|
+
produced_at: doc.structData?.produced_at ?? now,
|
|
44
49
|
};
|
|
45
|
-
// Compute freshness_tier
|
|
50
|
+
// Compute freshness_tier: TTL-based (explicit) or content-age (universal)
|
|
46
51
|
if (doc.structData?.ttl) {
|
|
47
52
|
const tier = computeFreshnessTier(doc.structData.ttl, doc.structData.extracted_at);
|
|
48
53
|
if (tier)
|
|
49
54
|
augmented.freshness_tier = tier;
|
|
50
55
|
}
|
|
56
|
+
else {
|
|
57
|
+
const tier = computeContentFreshnessTier(augmented.produced_at);
|
|
58
|
+
if (tier)
|
|
59
|
+
augmented.freshness_tier = tier;
|
|
60
|
+
}
|
|
51
61
|
augmentByType(doc, augmented);
|
|
52
62
|
return {
|
|
53
63
|
id: sanitizeId(doc.id),
|
|
@@ -58,6 +68,25 @@ export function toDeDocument(doc) {
|
|
|
58
68
|
},
|
|
59
69
|
};
|
|
60
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Universal content-age freshness tier (no TTL config required).
|
|
73
|
+
* Applies to all docs via produced_at — DE's boost-fresh/demote-expired
|
|
74
|
+
* controls handle the ranking impact.
|
|
75
|
+
*/
|
|
76
|
+
// TODO(knowledge-service): migrate alongside computeFreshnessTier when shared lib is ready
|
|
77
|
+
export function computeContentFreshnessTier(producedAt) {
|
|
78
|
+
if (!producedAt)
|
|
79
|
+
return undefined;
|
|
80
|
+
const ageMs = Date.now() - new Date(producedAt).getTime();
|
|
81
|
+
if (Number.isNaN(ageMs))
|
|
82
|
+
return undefined;
|
|
83
|
+
const ageDays = ageMs / (86_400_000);
|
|
84
|
+
if (ageDays < 7)
|
|
85
|
+
return "fresh";
|
|
86
|
+
if (ageDays >= 90)
|
|
87
|
+
return "stale";
|
|
88
|
+
return undefined; // neutral — no boost or penalty
|
|
89
|
+
}
|
|
61
90
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
91
|
// Type-Dispatched Augmentation
|
|
63
92
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -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,22 +123,32 @@ 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), audience (array), source, sources, status, signals.
|
|
127
|
+
// Documents have a mix of scope (singular string), scopes (plural array), and audience (v3).
|
|
128
|
+
// Filter must OR across all three to catch all docs.
|
|
129
|
+
// source for provenance, status for lifecycle.
|
|
130
|
+
const FILTERABLE_KEYS = new Set(["scope", "scopes", "audience", "source", "status"]);
|
|
125
131
|
function buildFilterExpression(filters, elevate) {
|
|
126
132
|
const parts = [];
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
133
|
+
// Documents have a mix of field names: scope (singular), scopes (plural array), audience (v3).
|
|
134
|
+
// OR across all three to catch every doc regardless of which field it uses.
|
|
135
|
+
// elevate=true removes scope filter (admin access).
|
|
130
136
|
const hasExplicitScope = filters?.scope && filters.scope.length > 0;
|
|
131
137
|
if (!elevate && !hasExplicitScope) {
|
|
132
138
|
const scopes = getActiveScopes();
|
|
133
139
|
const quoted = scopes.map((s) => `"${s}"`).join(",");
|
|
134
|
-
parts.push(`scope: ANY(${quoted})`);
|
|
140
|
+
parts.push(`(scope: ANY(${quoted}) OR scopes: ANY(${quoted}) OR audience: ANY(${quoted}))`);
|
|
135
141
|
}
|
|
136
142
|
if (filters) {
|
|
137
143
|
for (const [key, values] of Object.entries(filters)) {
|
|
138
144
|
if (!values || values.length === 0)
|
|
139
145
|
continue;
|
|
146
|
+
// Map caller's "scope" to all three field variants (OR)
|
|
147
|
+
if (key === "scope") {
|
|
148
|
+
const quoted = values.map((v) => `"${v}"`).join(",");
|
|
149
|
+
parts.push(`(scope: ANY(${quoted}) OR scopes: ANY(${quoted}) OR audience: ANY(${quoted}))`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
140
152
|
if (!FILTERABLE_KEYS.has(key))
|
|
141
153
|
continue;
|
|
142
154
|
const quoted = values.map((v) => `"${v}"`).join(",");
|
|
@@ -168,13 +180,30 @@ const QUERY_SIGNALS = [
|
|
|
168
180
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
181
|
// Dynamic Preamble — context-aware system instructions for :answer endpoint
|
|
170
182
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Base preamble applied to all answer queries.
|
|
185
|
+
*
|
|
186
|
+
* The Ema-specific context line applies to the primary use case (Ema platform knowledge).
|
|
187
|
+
* The code-accuracy rules are domain-agnostic and apply to ANY knowledge corpus that
|
|
188
|
+
* contains code examples, API formats, or structured data — they prevent the generative
|
|
189
|
+
* model from fabricating structures that look plausible but are wrong.
|
|
190
|
+
*/
|
|
172
191
|
const BASE_PREAMBLE = [
|
|
192
|
+
// Domain context (Ema-specific — other corpora would swap this line)
|
|
173
193
|
"You are answering questions about the Ema AI Employee platform — a workflow orchestration system.",
|
|
174
|
-
"When referencing actions or nodes, use their exact
|
|
175
|
-
|
|
194
|
+
"When referencing actions or nodes, use their exact names (e.g., chat_categorizer, respond_with_sources).",
|
|
195
|
+
// Code accuracy (domain-agnostic — applies to any technical corpus)
|
|
196
|
+
"CRITICAL — code accuracy rules:",
|
|
197
|
+
"1. NEVER assemble, fabricate, or synthesize complete code examples (JSON, YAML, config). The risk of structural errors is too high.",
|
|
198
|
+
"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.'",
|
|
199
|
+
"3. You MAY quote SHORT code fragments (under 5 lines) verbatim from source documents with attribution.",
|
|
200
|
+
"4. You MAY describe the structure in words (e.g., 'actions is an array of objects, each with name, action, and inputs').",
|
|
201
|
+
"5. For canonical formats, direct users to the authoritative source rather than reproducing it.",
|
|
176
202
|
"Cite specific document IDs when available.",
|
|
177
|
-
|
|
203
|
+
// Contextual guidance (extracted into structured fields by the handler)
|
|
204
|
+
"When documents have 'when_to_use' guidance, consider it when selecting which sources to cite — prefer documents whose when_to_use matches the query context.",
|
|
205
|
+
"Include relevant tips, hints, or next steps naturally within your answer based on the context and cited documents. Reference specific document IDs the reader can fetch for full details.",
|
|
206
|
+
].join("\n");
|
|
178
207
|
/** Query-pattern-specific preamble additions. */
|
|
179
208
|
const PREAMBLE_SIGNALS = [
|
|
180
209
|
{
|
|
@@ -183,7 +212,7 @@ const PREAMBLE_SIGNALS = [
|
|
|
183
212
|
},
|
|
184
213
|
{
|
|
185
214
|
pattern: /\b(how to|how do|pattern|example|wire|connect|build)\b/i,
|
|
186
|
-
addition: "Provide step-by-step instructions
|
|
215
|
+
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
216
|
},
|
|
188
217
|
{
|
|
189
218
|
pattern: /\b(rule|constraint|invariant|must|require|validate)\b/i,
|
|
@@ -246,25 +275,15 @@ function transformDeResponse(deResponse, mode) {
|
|
|
246
275
|
document: {
|
|
247
276
|
id: (structData.id ?? doc.id ?? ""),
|
|
248
277
|
name: (structData.name ?? ""),
|
|
278
|
+
// v3: description is the summary. Fall back to v2 fields for legacy docs.
|
|
249
279
|
type: (structData.type ?? structData.doc_type ?? ""),
|
|
250
280
|
scope: (structData.scope ?? "public"),
|
|
251
281
|
source: (structData.source ?? ""),
|
|
252
|
-
summary: (structData.summary ?? ""),
|
|
253
|
-
confidence_score: (structData.confidence_score ?? 0),
|
|
282
|
+
summary: (structData.description ?? structData.summary ?? ""),
|
|
254
283
|
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 } : {}),
|
|
284
|
+
// v3 fields
|
|
285
|
+
...(Array.isArray(structData.scopes) ? { scopes: structData.scopes } : {}),
|
|
286
|
+
...(Array.isArray(structData.sources) ? { sources: structData.sources } : {}),
|
|
268
287
|
// Non-promoted metadata only (no duplication)
|
|
269
288
|
structData: stripPromotedFields(structData),
|
|
270
289
|
},
|
|
@@ -478,22 +497,12 @@ async function searchDirect(query, options) {
|
|
|
478
497
|
// Dynamic domain boost — if query signals a specific platform, boost its domain
|
|
479
498
|
// and demote the other. DE serves both platforms; this keeps results focused.
|
|
480
499
|
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
|
-
];
|
|
500
|
+
// v3: confidence removed from structData. DE text matching handles relevance.
|
|
501
|
+
// Query-specific boosts (domain targeting) still apply.
|
|
491
502
|
const querySpecs = (queryBoost?.conditionBoostSpecs ?? []);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
];
|
|
496
|
-
body.boostSpec = { conditionBoostSpecs: allBoosts };
|
|
503
|
+
if (querySpecs.length > 0) {
|
|
504
|
+
body.boostSpec = { conditionBoostSpecs: querySpecs };
|
|
505
|
+
}
|
|
497
506
|
// Always request snippets — works with chunked datastores.
|
|
498
507
|
// (Extractive answers do NOT work with chunking, only snippets.)
|
|
499
508
|
// For answer mode, also request summary with citations.
|
|
@@ -936,7 +945,12 @@ export async function gcsFallbackSearch(query, options = {}) {
|
|
|
936
945
|
return false;
|
|
937
946
|
}
|
|
938
947
|
// Apply scope filter (matches DE behavior — default to active scopes)
|
|
939
|
-
|
|
948
|
+
// Check both scope (legacy) and audience (v3) fields
|
|
949
|
+
const docScope = doc.scope ?? "public";
|
|
950
|
+
const docAudience = "audience" in doc ? doc.audience : undefined;
|
|
951
|
+
const scopeMatch = activeScopes.includes(docScope);
|
|
952
|
+
const audienceMatch = docAudience?.some(a => activeScopes.includes(a)) ?? false;
|
|
953
|
+
if (!scopeMatch && !audienceMatch) {
|
|
940
954
|
return false;
|
|
941
955
|
}
|
|
942
956
|
return true;
|
package/dist/mcp/guidance.js
CHANGED
|
@@ -198,7 +198,7 @@ function formatToolSection(tg) {
|
|
|
198
198
|
}
|
|
199
199
|
if (tg.toolName === "feedback") {
|
|
200
200
|
extras.push("\nYour feedback is automatically collected and analyzed to improve the toolkit for all agents.");
|
|
201
|
-
extras.push("Include `knowledge_ref
|
|
201
|
+
extras.push("Include `knowledge_ref` to correlate feedback with specific knowledge documents — this drives automatic confidence scoring (documents with high negative feedback are auto-downgraded). Use `#fragment` for sub-document precision: `knowledge_ref=\"topic_auth#L51-L56\"` (lines), `knowledge_ref=\"topic_auth#authentication\"` (section). Same as GitHub URL fragments.");
|
|
202
202
|
}
|
|
203
203
|
return `## ${title}
|
|
204
204
|
Use the \`${tg.toolName}\` tool${tg.quickTip ? `. ${tg.quickTip}` : ""}:
|