Skip to content

Commit c259f41

Browse files
committed
Implement structured data generation and refactor tool handling
1 parent e37c1ed commit c259f41

8 files changed

Lines changed: 240 additions & 87 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {ActionSdk, HerculesFunctionContext} from "@code0-tech/hercules";
2+
import {Model} from "../types/aiModel";
3+
import {Tool} from "../types/aiTool";
4+
import {extractToken} from "../helpers";
5+
import {generateText, isLoopFinished, Output} from "ai";
6+
import {PlainValue} from "@code0-tech/tucana/helpers";
7+
import {buildProvider, buildTools} from "./helpers";
8+
import z from "zod";
9+
10+
11+
function generateZodSchema(value: PlainValue): z.ZodTypeAny {
12+
if (value === null) {
13+
return z.null();
14+
}
15+
16+
if (Array.isArray(value)) {
17+
if (value.length === 0) {
18+
return z.array(z.unknown());
19+
}
20+
21+
const itemSchemas = value.map((item) => generateZodSchema(item));
22+
const itemSchema =
23+
itemSchemas.length === 1
24+
? itemSchemas[0]
25+
: itemSchemas.slice(1).reduce((acc, current) => z.union([acc, current]), itemSchemas[0]);
26+
27+
return z.array(itemSchema);
28+
}
29+
30+
switch (typeof value) {
31+
case "string":
32+
return z.string();
33+
case "number":
34+
return z.number();
35+
case "boolean":
36+
return z.boolean();
37+
case "object": {
38+
const shape: Record<string, z.ZodTypeAny> = {};
39+
for (const [key, nestedValue] of Object.entries(value as Record<string, PlainValue>)) {
40+
shape[key] = generateZodSchema(nestedValue);
41+
}
42+
return z.object(shape);
43+
}
44+
default:
45+
return z.unknown();
46+
}
47+
}
48+
49+
export const handler = async (context: HerculesFunctionContext, model: Model, system: string, prompt: string, tools: Tool[], type: PlainValue): Promise<PlainValue> => {
50+
const apiKey = extractToken(context, model.provider)
51+
52+
const provider = buildProvider(model, apiKey);
53+
54+
const modelTools = await buildTools(tools, context);
55+
56+
const generated = await generateText({
57+
model: provider.languageModel(model.model),
58+
prompt: prompt,
59+
system: system,
60+
tools: modelTools,
61+
stopWhen: isLoopFinished(),
62+
output: Output.object({
63+
schema: generateZodSchema(type)
64+
})
65+
})
66+
67+
return generated.output as any
68+
};
69+
export default (sdk: ActionSdk) => {
70+
return sdk.registerRuntimeFunctionDefinitionsAndFunctionDefinitions(
71+
{
72+
definition: {
73+
runtimeName: "generateStructuredData",
74+
signature: "(model: AI_MODEL, system: string, prompt: string, tools: AI_TOOL[], type: TYPE): TYPE",
75+
linkedDataTypes: ["AI_MODEL", "AI_TOOL", "TYPE"],
76+
name: [
77+
{
78+
code: "en-US",
79+
content: "Generate structured Data"
80+
}
81+
],
82+
displayMessage: [
83+
{
84+
code: "en-US",
85+
content: "generate structured Data"
86+
}
87+
],
88+
parameters: [
89+
{
90+
runtimeName: "model",
91+
name: [
92+
{
93+
code: "en-US",
94+
content: "Model"
95+
}
96+
],
97+
},
98+
{
99+
runtimeName: "system",
100+
name: [
101+
{
102+
code: "en-US",
103+
content: "System prompt"
104+
}
105+
]
106+
},
107+
{
108+
runtimeName: "prompt",
109+
name: [
110+
{
111+
code: "en-US",
112+
content: "Prompt"
113+
}
114+
]
115+
},
116+
{
117+
runtimeName: "tools",
118+
name: [
119+
{
120+
code: "en-US",
121+
content: "Tools"
122+
}
123+
]
124+
},
125+
{
126+
runtimeName: "type",
127+
name: [
128+
{
129+
code: "en-US",
130+
content: "Output Type"
131+
}
132+
]
133+
}
134+
]
135+
},
136+
handler: handler
137+
}
138+
)
139+
}

actions/ai-action/src/functions/generateText.ts

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,17 @@
11
import {ActionSdk, HerculesFunctionContext} from "@code0-tech/hercules";
22
import {Model} from "../types/aiModel";
3-
import {extractToken, extractToolToken} from "../helpers";
4-
import {generateText, isLoopFinished, ToolSet} from "ai";
5-
import {createGoogleGenerativeAI} from "@ai-sdk/google";
6-
import {ProviderV3} from "@ai-sdk/provider";
7-
import {createOpenAI} from "@ai-sdk/openai";
8-
import {createAnthropic} from "@ai-sdk/anthropic";
9-
import {createOllama} from "ollama-ai-provider-v2";
10-
import {OllamaSettings, OllamaSettingsSchema} from "./models/ollama/ollamaSettings";
11-
import {createMCPClient} from "@ai-sdk/mcp";
3+
import {extractToken} from "../helpers";
4+
import {generateText, isLoopFinished} from "ai";
125
import {Tool} from "../types/aiTool";
6+
import {buildProvider, buildTools} from "./helpers";
137

148

159
export const handler = async (context: HerculesFunctionContext, model: Model, system: string, prompt: string, tools: Tool[]): Promise<string> => {
1610
const apiKey = extractToken(context, model.provider)
1711

18-
let provider: ProviderV3
12+
const provider = buildProvider(model, apiKey);
1913

20-
switch (model.provider) {
21-
case "google": {
22-
provider = createGoogleGenerativeAI({apiKey: apiKey});
23-
break
24-
}
25-
case "openai": {
26-
provider = createOpenAI({
27-
apiKey: apiKey
28-
})
29-
break
30-
}
31-
case "anthropic": {
32-
provider = createAnthropic({
33-
apiKey: apiKey
34-
})
35-
break
36-
}
37-
case "ollama": {
38-
const settings: OllamaSettings = OllamaSettingsSchema.parse(model.settings)
39-
provider = createOllama({
40-
baseURL: settings.baseURL
41-
})
42-
break
43-
}
44-
45-
}
46-
let modelTools: ToolSet = {}
47-
48-
for (const tool of tools) {
49-
const mcpClient = await createMCPClient({
50-
transport: {
51-
type: 'http',
52-
url: tool.url,
53-
headers: {Authorization: `Bearer ${extractToolToken(context, tool.providerName)}`},
54-
redirect: 'error',
55-
},
56-
});
57-
const tools = await mcpClient.tools()
58-
59-
if (!tool.tools) {
60-
modelTools = {
61-
...modelTools,
62-
...tools
63-
}
64-
continue
65-
}
66-
67-
const filteredTools: ToolSet = {}
68-
69-
Object.entries(tools)
70-
.filter(([toolName]) => tool.tools.includes(toolName))
71-
.forEach(([toolName, toolValue]) => {
72-
filteredTools[toolName] = toolValue
73-
})
74-
75-
76-
modelTools = {
77-
...modelTools,
78-
...filteredTools
79-
}
80-
}
14+
const modelTools = await buildTools(tools, context);
8115

8216
const generated = await generateText({
8317
model: provider.languageModel(model.model),
@@ -87,17 +21,9 @@ export const handler = async (context: HerculesFunctionContext, model: Model, sy
8721
stopWhen: isLoopFinished(),
8822
})
8923

90-
91-
if (generated.output) {
92-
return generated.output
93-
}
94-
generated.content.forEach((value: any) => {
95-
if (value.type === "tool-result" && value.output.content) {
96-
return value.output.content[0].text
97-
}
98-
})
99-
24+
return generated.output
10025
};
26+
10127
export default (sdk: ActionSdk) => {
10228
return sdk.registerRuntimeFunctionDefinitionsAndFunctionDefinitions(
10329
{
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {HerculesFunctionContext} from "@code0-tech/hercules";
2+
import {Model} from "../types/aiModel";
3+
import {Tool} from "../types/aiTool";
4+
import {extractToolToken} from "../helpers";
5+
import {ToolSet} from "ai";
6+
import {ProviderV3} from "@ai-sdk/provider";
7+
import {createGoogleGenerativeAI} from "@ai-sdk/google";
8+
import {createOpenAI} from "@ai-sdk/openai";
9+
import {createAnthropic} from "@ai-sdk/anthropic";
10+
import {OllamaSettings, OllamaSettingsSchema} from "./models/ollama/ollamaSettings";
11+
import {createOllama} from "ollama-ai-provider-v2";
12+
import {createMCPClient} from "@ai-sdk/mcp";
13+
14+
export function buildProvider(model: Model, apiKey: string): ProviderV3 {
15+
switch (model.provider) {
16+
case "google": {
17+
return createGoogleGenerativeAI({
18+
apiKey: apiKey
19+
});
20+
}
21+
case "openai": {
22+
return createOpenAI({
23+
apiKey: apiKey
24+
})
25+
break
26+
}
27+
case "anthropic": {
28+
return createAnthropic({
29+
apiKey: apiKey
30+
})
31+
}
32+
case "ollama": {
33+
const settings: OllamaSettings = OllamaSettingsSchema.parse(model.settings)
34+
return createOllama({
35+
baseURL: settings.baseURL
36+
})
37+
}
38+
39+
}
40+
}
41+
42+
export async function buildTools(tools: Tool[], context: HerculesFunctionContext) {
43+
let modelTools: ToolSet = {}
44+
45+
for (const tool of tools) {
46+
const mcpClient = await createMCPClient({
47+
transport: {
48+
type: 'http',
49+
url: tool.url,
50+
headers: {Authorization: `Bearer ${extractToolToken(context, tool.providerName)}`},
51+
redirect: 'error',
52+
},
53+
});
54+
const tools = await mcpClient.tools()
55+
56+
if (!tool.tools) {
57+
modelTools = {
58+
...modelTools,
59+
...tools
60+
}
61+
continue
62+
}
63+
64+
const filteredTools: ToolSet = {}
65+
66+
Object.entries(tools)
67+
.filter(([toolName]) => tool.tools.includes(toolName))
68+
.forEach(([toolName, toolValue]) => {
69+
filteredTools[toolName] = toolValue
70+
})
71+
72+
73+
modelTools = {
74+
...modelTools,
75+
...filteredTools
76+
}
77+
}
78+
return modelTools;
79+
}

actions/ai-action/src/types/aiTool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {singleZodSchemaToTypescriptDef} from "../../../../src/helpers";
55
export const TOOL_REGISTRY = {
66
github: {
77
url: "https://api.githubcopilot.com/mcp/",
8-
tools: ["add_comment_to_pending_review",
8+
tools: [
9+
"add_comment_to_pending_review",
910
"add_issue_comment",
1011
"add_reply_to_pull_request_comment",
1112
"assign_copilot_to_issue",

docs/Actions/AI/functions.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,9 +1006,9 @@ Return Type: [AI_MODEL](./types.mdx#ai_model)
10061006

10071007
#
10081008

1009-
| Parameter | Name | Type | Required | Description |
1010-
|-----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------|
1011-
| tools | | "add_comment_to_pending_review" \| "add_issue_comment" \| "add_reply_to_pull_request_comment" \| "assign_copilot_to_issue" \| "create_branch" \| "create_or_update_file" \| "create_pull_request" \| "create_pull_request_with_copilot" \| "create_repository" \| "delete_file" \| "fork_repository" \| "get_commit" \| "get_copilot_job_status" \| "get_file_contents" \| "get_label" \| "get_latest_release" \| "get_release_by_tag" \| "get_tag" \| "get_team_members" \| "get_teams" \| "issue_read" \| "issue_write" \| "list_branches" \| "list_commits" \| "list_issue_types" \| "list_issues" \| "list_pull_requests" \| "list_releases" \| "list_tags" \| "merge_pull_request" \| "pull_request_read" \| "pull_request_review_write" \| "push_files" \| "request_copilot_review" \| "run_secret_scanning" \| "search_code" \| "search_issues" \| "search_pull_requests" \| "search_repositories" \| "search_users" \| "sub_issue_write" \| "update_pull_request" \| "update_pull_request_branch" | No | |
1009+
| Parameter | Name | Type | Required | Description |
1010+
| --- | --- | --- | --- | --- |
1011+
| tools | | "add_comment_to_pending_review" \| "add_issue_comment" \| "add_reply_to_pull_request_comment" \| "assign_copilot_to_issue" \| "create_branch" \| "create_or_update_file" \| "create_pull_request" \| "create_pull_request_with_copilot" \| "create_repository" \| "delete_file" \| "fork_repository" \| "get_commit" \| "get_copilot_job_status" \| "get_file_contents" \| "get_label" \| "get_latest_release" \| "get_release_by_tag" \| "get_tag" \| "get_team_members" \| "get_teams" \| "issue_read" \| "issue_write" \| "list_branches" \| "list_commits" \| "list_issue_types" \| "list_issues" \| "list_pull_requests" \| "list_releases" \| "list_tags" \| "merge_pull_request" \| "pull_request_read" \| "pull_request_review_write" \| "push_files" \| "request_copilot_review" \| "run_secret_scanning" \| "search_code" \| "search_issues" \| "search_pull_requests" \| "search_repositories" \| "search_users" \| "sub_issue_write" \| "update_pull_request" \| "update_pull_request_branch" | No | |
10121012

10131013
Return Type: [AI_TOOL](./types.mdx#ai_tool)
10141014

package-lock.json

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
},
2828
"dependencies": {
2929
"@code0-tech/hercules": "^0.0.3",
30+
"@code0-tech/tucana": "^0.0.68",
3031
"axios": "^1.13.6",
3132
"vite": "^7.3.1",
3233
"zod": "^4.3.6",

src/standardActionDocs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import {Project, SymbolFlags, Type} from "ts-morph"
99

1010
interface Translation {
11-
code?: string
11+
code?: "en-US" | "de-DE" | string
1212
content?: string
1313
}
1414

0 commit comments

Comments
 (0)