Skip to content

Commit d055735

Browse files
authored
API now has action execution endpoint (#1797)
Using the API you can now supply actions that are forwarded to the dispatcher.
1 parent 6a775b5 commit d055735

File tree

8 files changed

+105
-27
lines changed

8 files changed

+105
-27
lines changed

ts/.vscode/launch.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@
6565
"cwd": "${workspaceFolder}/packages/api",
6666
"program": "./dist/index.js",
6767
"outFiles": ["${workspaceFolder}/**/*.js"],
68-
"resolveSourceMapLocations": []
68+
"console": "externalTerminal",
69+
"resolveSourceMapLocations": [],
70+
"serverReadyAction": {
71+
"pattern": "Listening on all local IPs at port ([0-9]+)",
72+
"uriFormat": "http://localhost:%s/chatView.html",
73+
"action": "openExternally"
74+
}
75+
}
6976
},
7077
{
7178
"type": "node",

ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"prettier": "^3.5.3",
5656
"shx": "^0.4.0"
5757
},
58-
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b",
58+
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
5959
"engines": {
6060
"node": ">=20",
6161
"pnpm": ">=10"

ts/packages/api/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ After setting up and building at the workspace root, there are several ways to s
1010

1111
### Locally
1212

13-
The server can be started with `npm run start` in this package's directory. Then connect to `http://localhost:3000` using a web browser.
13+
The server can be started with `npm run start` in this package's directory. Then connect to `http://localhost:3000` using a web browser. If you want to load the Shell interface in the browser window you want to open `http://localhost:3000/chatView.html`
1414

1515
### Docker Image
1616

ts/packages/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@typeagent/agent-rpc": "workspace:*",
3333
"@typeagent/agent-sdk": "workspace:*",
3434
"@typeagent/dispatcher-rpc": "workspace:*",
35+
"agent-cache": "workspace:*",
3536
"agent-dispatcher": "workspace:*",
3637
"aiclient": "workspace:*",
3738
"chalk": "^5.4.1",

ts/packages/api/src/typeAgentServer.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import { TypeAgentStorageProvider } from "./storageProvider.js";
2424
import { AzureStorageProvider } from "./storageProviders/azureStorageProvider.js";
2525
import { AWSStorageProvider } from "./storageProviders/awsStorageProvider.js";
2626
import { WebDispatcher, createWebDispatcher } from "./webDispatcher.js";
27+
import registerDebug from "debug";
28+
29+
const debug = registerDebug("typeagent:webserver:api");
30+
registerDebug.enable("typeagent:webserver:*");
2731

2832
export class TypeAgentServer {
2933
private webDispatcher: WebDispatcher | undefined;
@@ -34,12 +38,16 @@ export class TypeAgentServer {
3438
private config: TypeAgentAPIServerConfig;
3539

3640
constructor(private envPath: string) {
41+
debug(`Loading .env from path: ${envPath}`);
42+
3743
// typeAgent config
3844
dotenv.config({ path: this.envPath });
3945

4046
// web server config
4147
this.config = JSON.parse(readFileSync("data/config.json").toString());
4248

49+
debug(`Loaded web config from `);
50+
4351
const storageProviderMap = {
4452
azure: AzureStorageProvider,
4553
aws: AWSStorageProvider,
@@ -50,6 +58,11 @@ export class TypeAgentServer {
5058
this.storageProvider = new storageProviderMap[
5159
this.config.storageProvider
5260
]();
61+
debug(`Storage provider setup: ${this.config.storageProvider}`);
62+
} else {
63+
debug(
64+
`Skipping storage provider setup [backupEnabled: ${this.config.blobBackupEnabled}] [provider: ${this.config.storageProvider}]`,
65+
);
5366
}
5467
}
5568

@@ -60,39 +73,28 @@ export class TypeAgentServer {
6073
await this.syncFromProvider();
6174
this.startLocalStorageBackup();
6275
sw.stop("Downloaded Session Backup");
63-
/*
64-
if (
65-
this.storageAccount !== undefined &&
66-
this.storageAccount.length > 0 &&
67-
this.containerName != undefined &&
68-
this.containerName.length > 0
69-
) {
70-
const sw = new StopWatch();
71-
sw.start("Downloading Session Backup");
72-
73-
await this.syncBlobStorage();
74-
75-
this.startLocalStorageBackup();
76-
77-
sw.stop("Downloaded Session Backup");
78-
} else {
79-
console.warn(
80-
`Blob backup enabled but NOT configured. Missing env var ${openai.EnvVars.AZURE_STORAGE_ACCOUNT}.`,
81-
);
82-
}
83-
*/
8476
}
8577

8678
this.webDispatcher = await createWebDispatcher();
79+
debug("Web Dispatcher created.");
80+
8781
// web server
88-
this.webServer = new TypeAgentAPIWebServer(this.config);
82+
this.webServer = new TypeAgentAPIWebServer(
83+
this.config,
84+
(action: any) => {
85+
// a client passed in an action to perform
86+
this.webDispatcher?.handleAction(action);
87+
},
88+
);
8989
this.webServer.start();
90+
debug("TypeAgent Web Server created.");
9091

9192
// websocket server
9293
this.webSocketServer = new TypeAgentAPIWebSocketServer(
9394
this.webServer.server,
9495
this.webDispatcher.connect,
9596
);
97+
debug("WebSocket Server created.");
9698
}
9799

98100
stop() {

ts/packages/api/src/webDispatcher.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import { createDispatcher } from "agent-dispatcher";
4+
import { CommandResult, createDispatcher } from "agent-dispatcher";
55
import { getConsolePrompt } from "agent-dispatcher/helpers/console";
66
import { getInstanceDir, getClientId } from "agent-dispatcher/helpers/data";
77
import { getStatusSummary } from "agent-dispatcher/helpers/status";
@@ -15,11 +15,18 @@ import {
1515
} from "default-agent-provider";
1616
import WebSocket from "ws";
1717
import { getFsStorageProvider } from "dispatcher-node-providers";
18+
import registerDebug from "debug";
19+
import { FullAction } from "agent-cache";
20+
21+
const debug = registerDebug("typeagent:webserver:api");
22+
registerDebug.enable("typeagent:webserver:*");
1823

1924
export interface WebDispatcher {
2025
connect(ws: WebSocket): void;
2126
close(): void;
27+
handleAction(action: FullAction): Promise<CommandResult>;
2228
}
29+
2330
export async function createWebDispatcher(): Promise<WebDispatcher> {
2431
let ws: WebSocket | null = null;
2532
const clientIOChannel = createChannelAdapter((message: any) =>
@@ -31,6 +38,8 @@ export async function createWebDispatcher(): Promise<WebDispatcher> {
3138
),
3239
);
3340

41+
debug("Creating Web Dispatcher...");
42+
3443
const instanceDir = getInstanceDir();
3544
const clientIO = createClientIORpcClient(clientIOChannel.channel);
3645
const dispatcher = await createDispatcher("api", {
@@ -68,6 +77,16 @@ export async function createWebDispatcher(): Promise<WebDispatcher> {
6877
return newSettingSummary;
6978
};
7079

80+
async function handleAction(action: FullAction): Promise<any> {
81+
// TODO: expose executeAction so we can call that directly instead of running it through a command
82+
// TODO: bubble back any action results along with the command result
83+
await dispatcher.processCommand(
84+
`@action ${action.schemaName} ${action.actionName} --parameters '${JSON.stringify(action.parameters).replaceAll("'", "\\'")}'`,
85+
undefined,
86+
undefined,
87+
);
88+
}
89+
7190
async function processShellRequest(
7291
text: string,
7392
id: string,
@@ -91,6 +110,7 @@ export async function createWebDispatcher(): Promise<WebDispatcher> {
91110
const patchedDispatcher = {
92111
...dispatcher,
93112
processCommand: processShellRequest,
113+
handleAction: handleAction,
94114
};
95115

96116
const dispatcherChannel = createChannelAdapter((message: any) =>
@@ -138,5 +158,6 @@ export async function createWebDispatcher(): Promise<WebDispatcher> {
138158
close: () => {
139159
dispatcher.close();
140160
},
161+
handleAction: handleAction,
141162
};
142163
}

ts/packages/api/src/webServer.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ export type TypeAgentAPIServerConfig = {
2121
export class TypeAgentAPIWebServer {
2222
public server: Server<any, any>;
2323
private secureServer: SecureServer<any, any> | undefined;
24+
private actionHandler: (action: any) => any;
2425

25-
constructor(config: TypeAgentAPIServerConfig) {
26+
constructor(
27+
config: TypeAgentAPIServerConfig,
28+
actionHandler: (action: any) => any,
29+
) {
2630
// web server
2731
this.server = createServer((request: any, response: any) => {
2832
this.serve(config, request, response);
@@ -49,6 +53,9 @@ export class TypeAgentAPIWebServer {
4953
"SSL Certificates NOT found, cannot listen for https:// requests!",
5054
);
5155
}
56+
57+
// action handler
58+
this.actionHandler = actionHandler;
5259
}
5360

5461
serve(config: TypeAgentAPIServerConfig, request: any, response: any) {
@@ -64,6 +71,38 @@ export class TypeAgentAPIWebServer {
6471
return this.printHeaders(request, response);
6572
}
6673

74+
// process POST requests
75+
let url = new URL(request.url, "http://localhost");
76+
if (
77+
url.pathname == "/action/" &&
78+
(request.method === "PUT" || request.method === "GET")
79+
) {
80+
let data: string | null = "";
81+
if (request.method === "PUT") {
82+
data = request.read();
83+
} else {
84+
data = url.searchParams.get("a");
85+
}
86+
87+
try {
88+
var action: any = JSON.parse(data?.toString() ?? "");
89+
console.log(
90+
"Received action request: ",
91+
JSON.stringify(action, null, 2),
92+
);
93+
94+
var actionResult = this.actionHandler(action);
95+
96+
response.writeHead(200, { "Content-Type": "application/json" });
97+
response.end(JSON.stringify(actionResult));
98+
} catch (ex) {
99+
response.writeHead(500, { "Content-Type": "application/json" });
100+
response.end(JSON.stringify({ error: ex }));
101+
}
102+
103+
return;
104+
}
105+
67106
// make sure requested file falls under web root
68107
try {
69108
requestedFile = realpathSync(
@@ -85,13 +124,18 @@ export class TypeAgentAPIWebServer {
85124
response.end(readFileSync(requestedFile).toString());
86125

87126
console.log(`Served '${requestedFile}' as '${request.url}'`);
127+
return;
88128
}
89129
} catch (error) {
90130
response.writeHead(404, { "Content-Type": "text/plain" });
91131
response.end("File Not Found!\n");
92132

93133
console.log(`Unable to serve '${request.url}', 404. ${error}`);
134+
return;
94135
}
136+
137+
response.writeHead(400, { "Content-Type": "text/plain" });
138+
response.end("Invalid Request!\n");
95139
}
96140

97141
start() {

ts/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)