@vertesia/tools-sdk 1.0.0-dev.20260305.083323Z → 1.0.0-dev.20260331.091034Z
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/lib/cjs/ActivityCollection.js +93 -0
- package/lib/cjs/ActivityCollection.js.map +1 -0
- package/lib/cjs/auth.js +3 -3
- package/lib/cjs/auth.js.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/server/activities.js +103 -0
- package/lib/cjs/server/activities.js.map +1 -0
- package/lib/cjs/server/app-package.js +14 -1
- package/lib/cjs/server/app-package.js.map +1 -1
- package/lib/cjs/server/interactions.js +21 -9
- package/lib/cjs/server/interactions.js.map +1 -1
- package/lib/cjs/server/site.js +7 -1
- package/lib/cjs/server/site.js.map +1 -1
- package/lib/cjs/server/skills.js +10 -0
- package/lib/cjs/server/skills.js.map +1 -1
- package/lib/cjs/server.js +4 -1
- package/lib/cjs/server.js.map +1 -1
- package/lib/cjs/site/templates.js +138 -3
- package/lib/cjs/site/templates.js.map +1 -1
- package/lib/esm/ActivityCollection.js +89 -0
- package/lib/esm/ActivityCollection.js.map +1 -0
- package/lib/esm/auth.js +3 -3
- package/lib/esm/auth.js.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/server/activities.js +100 -0
- package/lib/esm/server/activities.js.map +1 -0
- package/lib/esm/server/app-package.js +14 -1
- package/lib/esm/server/app-package.js.map +1 -1
- package/lib/esm/server/interactions.js +21 -9
- package/lib/esm/server/interactions.js.map +1 -1
- package/lib/esm/server/site.js +8 -2
- package/lib/esm/server/site.js.map +1 -1
- package/lib/esm/server/skills.js +10 -0
- package/lib/esm/server/skills.js.map +1 -1
- package/lib/esm/server.js +4 -1
- package/lib/esm/server.js.map +1 -1
- package/lib/esm/site/templates.js +136 -3
- package/lib/esm/site/templates.js.map +1 -1
- package/lib/types/ActivityCollection.d.ts +55 -0
- package/lib/types/ActivityCollection.d.ts.map +1 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/server/activities.d.ts +4 -0
- package/lib/types/server/activities.d.ts.map +1 -0
- package/lib/types/server/app-package.d.ts.map +1 -1
- package/lib/types/server/interactions.d.ts.map +1 -1
- package/lib/types/server/site.d.ts.map +1 -1
- package/lib/types/server/types.d.ts +5 -0
- package/lib/types/server/types.d.ts.map +1 -1
- package/lib/types/server.d.ts.map +1 -1
- package/lib/types/site/templates.d.ts +10 -0
- package/lib/types/site/templates.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/ActivityCollection.test.ts +161 -0
- package/src/ActivityCollection.ts +136 -0
- package/src/auth.ts +3 -3
- package/src/index.ts +1 -0
- package/src/server/activities.test.ts +165 -0
- package/src/server/activities.ts +114 -0
- package/src/server/app-package.ts +15 -2
- package/src/server/interactions.ts +22 -11
- package/src/server/site.ts +9 -0
- package/src/server/skills.ts +10 -0
- package/src/server/types.ts +5 -0
- package/src/server.ts +4 -0
- package/src/site/templates.ts +143 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { RemoteActivityExecutionPayload } from "@vertesia/common";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ActivityCollection, ActivityDefinition } from "./ActivityCollection.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./auth.js", () => ({
|
|
6
|
+
authorize: vi.fn().mockResolvedValue({ token: "test-token" }),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const mockActivity: ActivityDefinition = {
|
|
10
|
+
name: "analyze_sentiment",
|
|
11
|
+
title: "Analyze Sentiment",
|
|
12
|
+
description: "Analyzes text sentiment",
|
|
13
|
+
input_schema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: { text: { type: "string" } },
|
|
16
|
+
required: ["text"],
|
|
17
|
+
},
|
|
18
|
+
output_schema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: { score: { type: "number" } },
|
|
21
|
+
},
|
|
22
|
+
run: vi.fn().mockResolvedValue({ score: 0.95 }),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const mockFailingActivity: ActivityDefinition = {
|
|
26
|
+
name: "fail_always",
|
|
27
|
+
description: "Always fails",
|
|
28
|
+
run: vi.fn().mockRejectedValue(new Error("Intentional failure")),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function createCollection(activities: ActivityDefinition[] = [mockActivity]) {
|
|
32
|
+
return new ActivityCollection({
|
|
33
|
+
name: "test-collection",
|
|
34
|
+
title: "Test Collection",
|
|
35
|
+
description: "A test collection",
|
|
36
|
+
activities,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("ActivityCollection", () => {
|
|
45
|
+
describe("getActivityDefinitions", () => {
|
|
46
|
+
it("returns correct metadata for all registered activities", () => {
|
|
47
|
+
const coll = createCollection([mockActivity, mockFailingActivity]);
|
|
48
|
+
const defs = coll.getActivityDefinitions();
|
|
49
|
+
|
|
50
|
+
expect(defs).toHaveLength(2);
|
|
51
|
+
expect(defs[0]).toEqual({
|
|
52
|
+
name: "analyze_sentiment",
|
|
53
|
+
title: "Analyze Sentiment",
|
|
54
|
+
description: "Analyzes text sentiment",
|
|
55
|
+
input_schema: mockActivity.input_schema,
|
|
56
|
+
output_schema: mockActivity.output_schema,
|
|
57
|
+
});
|
|
58
|
+
expect(defs[1]).toEqual({
|
|
59
|
+
name: "fail_always",
|
|
60
|
+
title: undefined,
|
|
61
|
+
description: "Always fails",
|
|
62
|
+
input_schema: undefined,
|
|
63
|
+
output_schema: undefined,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns empty array when no activities registered", () => {
|
|
68
|
+
const coll = createCollection([]);
|
|
69
|
+
expect(coll.getActivityDefinitions()).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("getActivity", () => {
|
|
74
|
+
it("returns the activity by name", () => {
|
|
75
|
+
const coll = createCollection();
|
|
76
|
+
const activity = coll.getActivity("analyze_sentiment");
|
|
77
|
+
expect(activity.name).toBe("analyze_sentiment");
|
|
78
|
+
expect(activity.run).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("throws 404 for unknown activity name", () => {
|
|
82
|
+
const coll = createCollection();
|
|
83
|
+
expect(() => coll.getActivity("nonexistent")).toThrow();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("iterator", () => {
|
|
88
|
+
it("iterates over all activities", () => {
|
|
89
|
+
const coll = createCollection([mockActivity, mockFailingActivity]);
|
|
90
|
+
const names = [...coll].map(a => a.name);
|
|
91
|
+
expect(names).toEqual(["analyze_sentiment", "fail_always"]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("execute", () => {
|
|
96
|
+
const createMockContext = () => {
|
|
97
|
+
const jsonFn = vi.fn().mockReturnThis();
|
|
98
|
+
return {
|
|
99
|
+
json: jsonFn,
|
|
100
|
+
req: {
|
|
101
|
+
header: vi.fn().mockReturnValue("Bearer test-token"),
|
|
102
|
+
url: "http://localhost/api/activities",
|
|
103
|
+
},
|
|
104
|
+
set: vi.fn(),
|
|
105
|
+
} as any;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const payload: RemoteActivityExecutionPayload = {
|
|
109
|
+
activity_name: "analyze_sentiment",
|
|
110
|
+
params: { text: "Hello world" },
|
|
111
|
+
metadata: {
|
|
112
|
+
app_install_id: "install-1",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it("calls the correct activity handler and returns result", async () => {
|
|
117
|
+
const coll = createCollection();
|
|
118
|
+
const ctx = createMockContext();
|
|
119
|
+
|
|
120
|
+
await coll.execute(ctx, payload);
|
|
121
|
+
|
|
122
|
+
expect(mockActivity.run).toHaveBeenCalledWith(
|
|
123
|
+
payload,
|
|
124
|
+
expect.objectContaining({ token: "test-token" }),
|
|
125
|
+
);
|
|
126
|
+
expect(ctx.json).toHaveBeenCalledWith({
|
|
127
|
+
result: { score: 0.95 },
|
|
128
|
+
is_error: false,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns is_error with message when activity throws", async () => {
|
|
133
|
+
const coll = createCollection([mockFailingActivity]);
|
|
134
|
+
const ctx = createMockContext();
|
|
135
|
+
|
|
136
|
+
const failPayload: RemoteActivityExecutionPayload = {
|
|
137
|
+
...payload,
|
|
138
|
+
activity_name: "fail_always",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await coll.execute(ctx, failPayload);
|
|
142
|
+
|
|
143
|
+
expect(ctx.json).toHaveBeenCalledWith(
|
|
144
|
+
{ result: null, is_error: true, error: "Intentional failure" },
|
|
145
|
+
500,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("throws 404 when activity_name is not found", async () => {
|
|
150
|
+
const coll = createCollection();
|
|
151
|
+
const ctx = createMockContext();
|
|
152
|
+
|
|
153
|
+
const unknownPayload: RemoteActivityExecutionPayload = {
|
|
154
|
+
...payload,
|
|
155
|
+
activity_name: "nonexistent",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
await expect(coll.execute(ctx, unknownPayload)).rejects.toThrow();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { RemoteActivityDefinition, RemoteActivityExecutionPayload, RemoteActivityExecutionResponse } from "@vertesia/common";
|
|
2
|
+
import { Context } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { authorize } from "./auth.js";
|
|
5
|
+
import { CollectionProperties, ICollection, ToolExecutionContext } from "./types.js";
|
|
6
|
+
import { kebabCaseToTitle } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Context provided to activity handlers during execution.
|
|
10
|
+
* Same interface as ToolExecutionContext: includes token, decoded payload, and getClient().
|
|
11
|
+
*/
|
|
12
|
+
export type ActivityExecutionContext = ToolExecutionContext;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Function signature for a remote activity handler.
|
|
16
|
+
*/
|
|
17
|
+
export type ActivityFn<ParamsT extends Record<string, any> = Record<string, any>> = (
|
|
18
|
+
payload: RemoteActivityExecutionPayload<ParamsT>,
|
|
19
|
+
context: ActivityExecutionContext
|
|
20
|
+
) => Promise<any>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An activity definition within an ActivityCollection.
|
|
24
|
+
*/
|
|
25
|
+
export interface ActivityDefinition<ParamsT extends Record<string, any> = Record<string, any>> {
|
|
26
|
+
/** Activity name (snake_case) */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Display title */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Description of what the activity does */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** JSON Schema for the activity input parameters */
|
|
33
|
+
input_schema?: Record<string, any>;
|
|
34
|
+
/** JSON Schema for the activity output */
|
|
35
|
+
output_schema?: Record<string, any>;
|
|
36
|
+
/** The activity handler function */
|
|
37
|
+
run: ActivityFn<ParamsT>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ActivityCollectionProperties extends CollectionProperties {
|
|
41
|
+
activities: ActivityDefinition[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A collection of remote activities exposed by a tool server for DSL workflows.
|
|
46
|
+
* Follows the same collection pattern as ToolCollection and SkillCollection.
|
|
47
|
+
*/
|
|
48
|
+
export class ActivityCollection implements ICollection<ActivityDefinition> {
|
|
49
|
+
name: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
icon?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
private registry: Record<string, ActivityDefinition> = {};
|
|
54
|
+
|
|
55
|
+
constructor({ name, title, icon, description, activities }: ActivityCollectionProperties) {
|
|
56
|
+
this.name = name;
|
|
57
|
+
this.title = title || kebabCaseToTitle(name);
|
|
58
|
+
this.icon = icon;
|
|
59
|
+
this.description = description;
|
|
60
|
+
for (const activity of activities) {
|
|
61
|
+
this.registry[activity.name] = activity;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[Symbol.iterator](): Iterator<ActivityDefinition> {
|
|
66
|
+
let index = 0;
|
|
67
|
+
const activities = Object.values(this.registry);
|
|
68
|
+
return {
|
|
69
|
+
next(): IteratorResult<ActivityDefinition> {
|
|
70
|
+
if (index < activities.length) {
|
|
71
|
+
return { value: activities[index++], done: false };
|
|
72
|
+
}
|
|
73
|
+
return { done: true, value: undefined };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get activity definitions for discovery (metadata only, no run function).
|
|
80
|
+
*/
|
|
81
|
+
getActivityDefinitions(): RemoteActivityDefinition[] {
|
|
82
|
+
return Object.values(this.registry).map(activity => ({
|
|
83
|
+
name: activity.name,
|
|
84
|
+
title: activity.title,
|
|
85
|
+
description: activity.description,
|
|
86
|
+
input_schema: activity.input_schema,
|
|
87
|
+
output_schema: activity.output_schema,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getActivity(name: string): ActivityDefinition {
|
|
92
|
+
const activity = this.registry[name];
|
|
93
|
+
if (!activity) {
|
|
94
|
+
throw new HTTPException(404, {
|
|
95
|
+
message: `Activity not found: ${name}. Available: ${Object.keys(this.registry).join(', ')}`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return activity;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Execute an activity from an HTTP POST request.
|
|
103
|
+
*/
|
|
104
|
+
async execute(ctx: Context, payload: RemoteActivityExecutionPayload): Promise<Response> {
|
|
105
|
+
const activityName = payload.activity_name;
|
|
106
|
+
|
|
107
|
+
console.debug(`[ActivityCollection] Activity call received: ${activityName}`, {
|
|
108
|
+
collection: this.name,
|
|
109
|
+
metadata: payload.metadata,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const activity = this.getActivity(activityName);
|
|
113
|
+
|
|
114
|
+
const context = await authorize(ctx, payload.metadata?.endpoints);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = await activity.run(payload, context);
|
|
118
|
+
return ctx.json({
|
|
119
|
+
result,
|
|
120
|
+
is_error: false,
|
|
121
|
+
} satisfies RemoteActivityExecutionResponse);
|
|
122
|
+
} catch (err: unknown) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
console.error(`[ActivityCollection] Activity execution failed: ${activityName}`, {
|
|
125
|
+
collection: this.name,
|
|
126
|
+
metadata: payload.metadata,
|
|
127
|
+
error: message,
|
|
128
|
+
});
|
|
129
|
+
return ctx.json({
|
|
130
|
+
result: null,
|
|
131
|
+
is_error: true,
|
|
132
|
+
error: message,
|
|
133
|
+
} satisfies RemoteActivityExecutionResponse, 500);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/auth.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { ToolExecutionContext } from "./types.js";
|
|
|
7
7
|
const cache: Record<string, JWTVerifyGetKey> = {};
|
|
8
8
|
|
|
9
9
|
export async function getJwks(url: string) {
|
|
10
|
-
if (!cache
|
|
10
|
+
if (!cache[url]) {
|
|
11
11
|
console.log('JWKS cache miss for: ', url);
|
|
12
12
|
const jwks = await fetch(url).then(r => {
|
|
13
13
|
if (r.ok) {
|
|
@@ -17,9 +17,9 @@ export async function getJwks(url: string) {
|
|
|
17
17
|
}).catch(err => {
|
|
18
18
|
throw new Error("Failed to fetch jwks: " + err.message);
|
|
19
19
|
})
|
|
20
|
-
cache
|
|
20
|
+
cache[url] = createLocalJWKSet(jwks);
|
|
21
21
|
}
|
|
22
|
-
return cache
|
|
22
|
+
return cache[url];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function verifyToken(token: string) {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ActivityCollection, ActivityDefinition } from "../ActivityCollection.js";
|
|
4
|
+
import { createActivitiesRoute } from "./activities.js";
|
|
5
|
+
import { ToolServerConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// Mock authorize to avoid JWT verification
|
|
8
|
+
vi.mock("../auth.js", () => ({
|
|
9
|
+
authorize: vi.fn().mockResolvedValue({ token: "test-token" }),
|
|
10
|
+
AuthSession: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockActivity: ActivityDefinition = {
|
|
14
|
+
name: "analyze_sentiment",
|
|
15
|
+
title: "Analyze Sentiment",
|
|
16
|
+
description: "Analyzes text sentiment",
|
|
17
|
+
input_schema: { type: "object", properties: { text: { type: "string" } } },
|
|
18
|
+
run: vi.fn().mockResolvedValue({ score: 0.95 }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockActivity2: ActivityDefinition = {
|
|
22
|
+
name: "extract_entities",
|
|
23
|
+
description: "Extracts entities",
|
|
24
|
+
run: vi.fn().mockResolvedValue({ entities: ["foo"] }),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function createApp(activities: ActivityCollection[] = []) {
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
const config = { activities } as ToolServerConfig;
|
|
30
|
+
createActivitiesRoute(app, "/api/activities", config);
|
|
31
|
+
return app;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createTestCollections() {
|
|
35
|
+
const coll1 = new ActivityCollection({
|
|
36
|
+
name: "nlp",
|
|
37
|
+
description: "NLP activities",
|
|
38
|
+
activities: [mockActivity],
|
|
39
|
+
});
|
|
40
|
+
const coll2 = new ActivityCollection({
|
|
41
|
+
name: "extraction",
|
|
42
|
+
description: "Extraction activities",
|
|
43
|
+
activities: [mockActivity2],
|
|
44
|
+
});
|
|
45
|
+
return [coll1, coll2];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("Activities server routes", () => {
|
|
49
|
+
describe("GET /api/activities", () => {
|
|
50
|
+
it("returns all activity definitions across collections", async () => {
|
|
51
|
+
const app = createApp(createTestCollections());
|
|
52
|
+
const res = await app.request("/api/activities");
|
|
53
|
+
const body = await res.json() as any;
|
|
54
|
+
|
|
55
|
+
expect(res.status).toBe(200);
|
|
56
|
+
expect(body.activities).toHaveLength(2);
|
|
57
|
+
expect(body.activities[0].name).toBe("analyze_sentiment");
|
|
58
|
+
expect(body.activities[1].name).toBe("extract_entities");
|
|
59
|
+
expect(body.collections).toHaveLength(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns empty when no collections configured", async () => {
|
|
63
|
+
const app = createApp([]);
|
|
64
|
+
const res = await app.request("/api/activities");
|
|
65
|
+
const body = await res.json() as any;
|
|
66
|
+
|
|
67
|
+
expect(res.status).toBe(200);
|
|
68
|
+
expect(body.activities).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("GET /api/activities/{collection}", () => {
|
|
73
|
+
it("returns activities for a specific collection", async () => {
|
|
74
|
+
const app = createApp(createTestCollections());
|
|
75
|
+
const res = await app.request("/api/activities/nlp");
|
|
76
|
+
const body = await res.json() as any;
|
|
77
|
+
|
|
78
|
+
expect(res.status).toBe(200);
|
|
79
|
+
expect(body.name).toBe("nlp");
|
|
80
|
+
expect(body.activities).toHaveLength(1);
|
|
81
|
+
expect(body.activities[0].name).toBe("analyze_sentiment");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("POST /api/activities", () => {
|
|
86
|
+
it("executes activity by name", async () => {
|
|
87
|
+
const app = createApp(createTestCollections());
|
|
88
|
+
const res = await app.request("/api/activities", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"Authorization": "Bearer test-token",
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
activity_name: "analyze_sentiment",
|
|
96
|
+
params: { text: "hello" },
|
|
97
|
+
metadata: {
|
|
98
|
+
workflow_name: "wf",
|
|
99
|
+
account_id: "acc",
|
|
100
|
+
project_id: "proj",
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(res.status).toBe(200);
|
|
106
|
+
const body = await res.json() as any;
|
|
107
|
+
expect(body.result).toEqual({ score: 0.95 });
|
|
108
|
+
expect(body.is_error).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns 404 for unknown activity name", async () => {
|
|
112
|
+
const app = createApp(createTestCollections());
|
|
113
|
+
const res = await app.request("/api/activities", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
"Authorization": "Bearer test-token",
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
activity_name: "nonexistent",
|
|
121
|
+
params: {},
|
|
122
|
+
metadata: {},
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(res.status).toBe(404);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns 400 for missing activity_name", async () => {
|
|
130
|
+
const app = createApp(createTestCollections());
|
|
131
|
+
const res = await app.request("/api/activities", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
"Authorization": "Bearer test-token",
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({ params: {} }),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(res.status).toBe(400);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("POST /api/activities/{collection}", () => {
|
|
145
|
+
it("routes to correct collection", async () => {
|
|
146
|
+
const app = createApp(createTestCollections());
|
|
147
|
+
const res = await app.request("/api/activities/extraction", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"Authorization": "Bearer test-token",
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
activity_name: "extract_entities",
|
|
155
|
+
params: {},
|
|
156
|
+
metadata: {},
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(res.status).toBe(200);
|
|
161
|
+
const body = await res.json() as any;
|
|
162
|
+
expect(body.result).toEqual({ entities: ["foo"] });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { RemoteActivityDefinition, RemoteActivityExecutionPayload } from "@vertesia/common";
|
|
2
|
+
import { Context, Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { ActivityCollection } from "../ActivityCollection.js";
|
|
5
|
+
import { ToolServerConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Safely parse JSON from a request body. Throws HTTPException(400) on invalid JSON.
|
|
9
|
+
*/
|
|
10
|
+
async function safeParseJson(c: Context): Promise<unknown> {
|
|
11
|
+
try {
|
|
12
|
+
return await c.req.json();
|
|
13
|
+
} catch {
|
|
14
|
+
throw new HTTPException(400, {
|
|
15
|
+
message: 'Invalid JSON in request body.'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates the structure of a RemoteActivityExecutionPayload.
|
|
22
|
+
* Returns the parsed payload or throws HTTPException(400).
|
|
23
|
+
*/
|
|
24
|
+
function parseActivityPayload(body: unknown): RemoteActivityExecutionPayload {
|
|
25
|
+
if (!body || typeof body !== 'object') {
|
|
26
|
+
throw new HTTPException(400, {
|
|
27
|
+
message: 'Invalid or missing activity execution payload.'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const obj = body as Record<string, any>;
|
|
31
|
+
if (typeof obj.activity_name !== 'string' || !obj.activity_name) {
|
|
32
|
+
throw new HTTPException(400, {
|
|
33
|
+
message: 'Missing required field: activity_name'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
activity_name: obj.activity_name,
|
|
38
|
+
params: obj.params || {},
|
|
39
|
+
metadata: obj.metadata || {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createActivitiesRoute(app: Hono, basePath: string, config: ToolServerConfig) {
|
|
44
|
+
const { activities = [] } = config;
|
|
45
|
+
|
|
46
|
+
// Build a map of activity name -> collection for routing
|
|
47
|
+
const activityToCollection = new Map<string, ActivityCollection>();
|
|
48
|
+
for (const coll of activities) {
|
|
49
|
+
for (const def of coll.getActivityDefinitions()) {
|
|
50
|
+
activityToCollection.set(def.name, coll);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// GET /api/activities - List all activities across all collections
|
|
55
|
+
app.get(basePath, (c) => {
|
|
56
|
+
const allActivities: RemoteActivityDefinition[] = [];
|
|
57
|
+
for (const coll of activities) {
|
|
58
|
+
allActivities.push(...coll.getActivityDefinitions());
|
|
59
|
+
}
|
|
60
|
+
return c.json({
|
|
61
|
+
title: 'All Activities',
|
|
62
|
+
description: 'All available remote activities across all collections',
|
|
63
|
+
activities: allActivities,
|
|
64
|
+
collections: activities.map(a => ({
|
|
65
|
+
name: a.name,
|
|
66
|
+
title: a.title,
|
|
67
|
+
description: a.description,
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// POST /api/activities - Execute an activity by name (routes to correct collection)
|
|
73
|
+
app.post(basePath, async (c) => {
|
|
74
|
+
const body = await safeParseJson(c);
|
|
75
|
+
const payload = parseActivityPayload(body);
|
|
76
|
+
|
|
77
|
+
const collection = activityToCollection.get(payload.activity_name);
|
|
78
|
+
if (!collection) {
|
|
79
|
+
throw new HTTPException(404, {
|
|
80
|
+
message: `Activity not found: ${payload.activity_name}. Available: ${Array.from(activityToCollection.keys()).join(', ')}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return collection.execute(c, payload);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Per-collection endpoints
|
|
88
|
+
for (const coll of activities) {
|
|
89
|
+
app.route(`${basePath}/${coll.name}`, createActivityEndpoints(coll));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createActivityEndpoints(coll: ActivityCollection): Hono {
|
|
94
|
+
const endpoint = new Hono();
|
|
95
|
+
|
|
96
|
+
// GET /api/activities/{collection} - List activities in this collection
|
|
97
|
+
endpoint.get('/', (c) => {
|
|
98
|
+
return c.json({
|
|
99
|
+
name: coll.name,
|
|
100
|
+
title: coll.title,
|
|
101
|
+
description: coll.description,
|
|
102
|
+
activities: coll.getActivityDefinitions(),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// POST /api/activities/{collection} - Execute activity in this collection
|
|
107
|
+
endpoint.post('/', async (c: Context) => {
|
|
108
|
+
const body = await safeParseJson(c);
|
|
109
|
+
const payload = parseActivityPayload(body);
|
|
110
|
+
return coll.execute(c, payload);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return endpoint;
|
|
114
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppPackage, AppPackageScope, AppWidgetInfo, CatalogInteractionRef, InCodeTypeDefinition } from "@vertesia/common";
|
|
1
|
+
import { AppPackage, AppPackageScope, AppWidgetInfo, CatalogInteractionRef, InCodeTypeDefinition, RemoteActivityDefinition } from "@vertesia/common";
|
|
2
2
|
import { Context, Hono } from "hono";
|
|
3
3
|
import { ToolUseContext } from "../types.js";
|
|
4
4
|
import { ToolServerConfig } from "./types.js";
|
|
@@ -103,6 +103,15 @@ const builders: Record<Exclude<AppPackageScope, 'all'>, (pkg: AppPackage, config
|
|
|
103
103
|
pkg.settings_schema = { ...config.settings };
|
|
104
104
|
}
|
|
105
105
|
},
|
|
106
|
+
async activities(pkg: AppPackage, config: ToolServerConfig) {
|
|
107
|
+
const allActivities: RemoteActivityDefinition[] = [];
|
|
108
|
+
for (const coll of config.activities || []) {
|
|
109
|
+
for (const def of coll.getActivityDefinitions()) {
|
|
110
|
+
allActivities.push({ ...def, collection: coll.name });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
pkg.activities = allActivities;
|
|
114
|
+
},
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
|
|
@@ -120,6 +129,7 @@ async function handlePackageRequest(c: Context, config: ToolServerConfig) {
|
|
|
120
129
|
await builders.widgets(pkg, config, c);
|
|
121
130
|
await builders.ui(pkg, config, c);
|
|
122
131
|
await builders.settings(pkg, config, c);
|
|
132
|
+
await builders.activities(pkg, config, c);
|
|
123
133
|
} else {
|
|
124
134
|
if (scopes.has('tools')) {
|
|
125
135
|
await builders.tools(pkg, config, c);
|
|
@@ -140,7 +150,10 @@ async function handlePackageRequest(c: Context, config: ToolServerConfig) {
|
|
|
140
150
|
await builders.ui(pkg, config, c);
|
|
141
151
|
}
|
|
142
152
|
if (scopes.has('settings')) {
|
|
143
|
-
builders.settings(pkg, config, c);
|
|
153
|
+
await builders.settings(pkg, config, c);
|
|
154
|
+
}
|
|
155
|
+
if (scopes.has('activities')) {
|
|
156
|
+
await builders.activities(pkg, config, c);
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
|