@amitdeshmukh/ax-crew 8.0.0 → 8.0.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/agents/agentConfig.js +7 -1
- package/dist/agents/index.js +17 -1
- package/dist/metrics/registry.d.ts +4 -1
- package/dist/metrics/registry.js +32 -2
- package/dist/metrics/types.d.ts +7 -0
- package/examples/factory.ts +174 -0
- package/examples/google-gemini-test.ts +291 -0
- package/examples/graphjin-README.md +167 -0
- package/examples/graphjin-database-agent.ts +141 -0
- package/examples/planner.ts +107 -0
- package/package.json +1 -1
- package/src/agents/agentConfig.ts +7 -1
- package/src/agents/index.ts +21 -1
- package/src/metrics/registry.ts +37 -3
- package/src/metrics/types.ts +8 -0
- package/plan.md +0 -255
|
@@ -54,7 +54,13 @@ const initializeMCPServers = async (agentConfigData) => {
|
|
|
54
54
|
const mcpClient = new AxMCPClient(transport, { debug: agentConfigData.debug || false });
|
|
55
55
|
await mcpClient.init();
|
|
56
56
|
initializedClients.push(mcpClient);
|
|
57
|
-
|
|
57
|
+
// Normalize MCP tool schemas: some MCP servers omit `parameters` for
|
|
58
|
+
// zero-arg tools, but providers like Gemini require a valid schema.
|
|
59
|
+
const mcpFns = mcpClient.toFunction().map(fn => ({
|
|
60
|
+
...fn,
|
|
61
|
+
parameters: fn.parameters ?? { type: 'object', properties: {} },
|
|
62
|
+
}));
|
|
63
|
+
functions.push(...mcpFns);
|
|
58
64
|
}
|
|
59
65
|
return functions;
|
|
60
66
|
}
|
package/dist/agents/index.js
CHANGED
|
@@ -513,6 +513,22 @@ class AxCrew {
|
|
|
513
513
|
uniqueFunctions.push(fn);
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
|
+
// Wrap each function handler to record call count and latency in MetricsRegistry
|
|
517
|
+
const crewId = this.crewId;
|
|
518
|
+
const agentNameForMetrics = name;
|
|
519
|
+
const instrumentedFunctions = uniqueFunctions.map(fn => ({
|
|
520
|
+
...fn,
|
|
521
|
+
func: async (args, extra) => {
|
|
522
|
+
const fnStart = performance.now();
|
|
523
|
+
try {
|
|
524
|
+
return await fn.func(args, extra);
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
const latencyMs = performance.now() - fnStart;
|
|
528
|
+
MetricsRegistry.recordFunctionCall({ crewId, agent: agentNameForMetrics }, latencyMs, fn.name);
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
}));
|
|
516
532
|
// Create an instance of StatefulAxAgent
|
|
517
533
|
// Set crew reference in state for execution tracking (ACE feedback routing)
|
|
518
534
|
const agentState = { ...this.state, crew: this };
|
|
@@ -521,7 +537,7 @@ class AxCrew {
|
|
|
521
537
|
description,
|
|
522
538
|
definition: agentConfig.definition,
|
|
523
539
|
signature,
|
|
524
|
-
functions:
|
|
540
|
+
functions: instrumentedFunctions,
|
|
525
541
|
agents: uniqueSubAgents,
|
|
526
542
|
examples,
|
|
527
543
|
debug: agentConfig.debug,
|
|
@@ -20,8 +20,11 @@ export declare function recordTokens(labels: LabelKeys, usage: TokenUsage): void
|
|
|
20
20
|
export declare function recordEstimatedCost(labels: LabelKeys, usd: number): void;
|
|
21
21
|
/**
|
|
22
22
|
* Record a function call invocation and add its latency to totals.
|
|
23
|
+
* @param labels Crew/agent identifiers
|
|
24
|
+
* @param latencyMs Duration of the function call in milliseconds
|
|
25
|
+
* @param functionName Optional name of the function that was called
|
|
23
26
|
*/
|
|
24
|
-
export declare function recordFunctionCall(labels: LabelKeys, latencyMs: number): void;
|
|
27
|
+
export declare function recordFunctionCall(labels: LabelKeys, latencyMs: number, functionName?: string): void;
|
|
25
28
|
/**
|
|
26
29
|
* Get a metrics snapshot for specific labels (crew + agent + optional provider/model).
|
|
27
30
|
*/
|
package/dist/metrics/registry.js
CHANGED
|
@@ -19,6 +19,7 @@ function getOrInit(labels) {
|
|
|
19
19
|
estimatedCostUSD: 0,
|
|
20
20
|
functionCalls: 0,
|
|
21
21
|
functionLatencyMs: 0,
|
|
22
|
+
functionDetails: new Map(),
|
|
22
23
|
};
|
|
23
24
|
store.set(k, c);
|
|
24
25
|
}
|
|
@@ -64,11 +65,20 @@ export function recordEstimatedCost(labels, usd) {
|
|
|
64
65
|
}
|
|
65
66
|
/**
|
|
66
67
|
* Record a function call invocation and add its latency to totals.
|
|
68
|
+
* @param labels Crew/agent identifiers
|
|
69
|
+
* @param latencyMs Duration of the function call in milliseconds
|
|
70
|
+
* @param functionName Optional name of the function that was called
|
|
67
71
|
*/
|
|
68
|
-
export function recordFunctionCall(labels, latencyMs) {
|
|
72
|
+
export function recordFunctionCall(labels, latencyMs, functionName) {
|
|
69
73
|
const c = getOrInit(labels);
|
|
70
74
|
c.functionCalls += 1;
|
|
71
75
|
c.functionLatencyMs += latencyMs || 0;
|
|
76
|
+
if (functionName) {
|
|
77
|
+
const detail = c.functionDetails.get(functionName) || { calls: 0, latencyMs: 0 };
|
|
78
|
+
detail.calls += 1;
|
|
79
|
+
detail.latencyMs += latencyMs || 0;
|
|
80
|
+
c.functionDetails.set(functionName, detail);
|
|
81
|
+
}
|
|
72
82
|
}
|
|
73
83
|
/**
|
|
74
84
|
* Get a metrics snapshot for specific labels (crew + agent + optional provider/model).
|
|
@@ -96,9 +106,20 @@ export function snapshot(labels) {
|
|
|
96
106
|
functions: {
|
|
97
107
|
totalFunctionCalls: c.functionCalls,
|
|
98
108
|
totalFunctionLatencyMs: c.functionLatencyMs,
|
|
109
|
+
details: detailsFromMap(c.functionDetails),
|
|
99
110
|
},
|
|
100
111
|
};
|
|
101
112
|
}
|
|
113
|
+
/** Convert internal function detail map to sorted array */
|
|
114
|
+
function detailsFromMap(m) {
|
|
115
|
+
if (m.size === 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
return Array.from(m.entries()).map(([name, d]) => ({
|
|
118
|
+
name,
|
|
119
|
+
calls: d.calls,
|
|
120
|
+
totalLatencyMs: d.latencyMs,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
102
123
|
/**
|
|
103
124
|
* Reset metrics for specific labels, or clear all if no labels provided.
|
|
104
125
|
*/
|
|
@@ -125,6 +146,7 @@ export function snapshotCrew(crewId) {
|
|
|
125
146
|
estimatedCostUSD: 0,
|
|
126
147
|
functionCalls: 0,
|
|
127
148
|
functionLatencyMs: 0,
|
|
149
|
+
functionDetails: new Map(),
|
|
128
150
|
};
|
|
129
151
|
const agg = Array.from(store.entries()).reduce((acc, [k, v]) => {
|
|
130
152
|
if (k.startsWith(crewId + '|')) {
|
|
@@ -138,9 +160,16 @@ export function snapshotCrew(crewId) {
|
|
|
138
160
|
acc.estimatedCostUSD = Number(new Big(acc.estimatedCostUSD || 0).plus(v.estimatedCostUSD || 0));
|
|
139
161
|
acc.functionCalls += v.functionCalls;
|
|
140
162
|
acc.functionLatencyMs += v.functionLatencyMs;
|
|
163
|
+
// Merge per-function details
|
|
164
|
+
for (const [fnName, d] of v.functionDetails) {
|
|
165
|
+
const existing = acc.functionDetails.get(fnName) || { calls: 0, latencyMs: 0 };
|
|
166
|
+
existing.calls += d.calls;
|
|
167
|
+
existing.latencyMs += d.latencyMs;
|
|
168
|
+
acc.functionDetails.set(fnName, existing);
|
|
169
|
+
}
|
|
141
170
|
}
|
|
142
171
|
return acc;
|
|
143
|
-
},
|
|
172
|
+
}, empty);
|
|
144
173
|
const totalTokens = agg.inputTokens + agg.outputTokens;
|
|
145
174
|
return {
|
|
146
175
|
requests: {
|
|
@@ -160,6 +189,7 @@ export function snapshotCrew(crewId) {
|
|
|
160
189
|
functions: {
|
|
161
190
|
totalFunctionCalls: agg.functionCalls,
|
|
162
191
|
totalFunctionLatencyMs: agg.functionLatencyMs,
|
|
192
|
+
details: detailsFromMap(agg.functionDetails),
|
|
163
193
|
},
|
|
164
194
|
};
|
|
165
195
|
}
|
package/dist/metrics/types.d.ts
CHANGED
|
@@ -15,9 +15,16 @@ export interface RequestStats {
|
|
|
15
15
|
durationMsSum: number;
|
|
16
16
|
durationCount: number;
|
|
17
17
|
}
|
|
18
|
+
export interface FunctionCallDetail {
|
|
19
|
+
name: string;
|
|
20
|
+
calls: number;
|
|
21
|
+
totalLatencyMs: number;
|
|
22
|
+
}
|
|
18
23
|
export interface FunctionStats {
|
|
19
24
|
totalFunctionCalls: number;
|
|
20
25
|
totalFunctionLatencyMs: number;
|
|
26
|
+
/** Per-function breakdown of calls and latency */
|
|
27
|
+
details?: FunctionCallDetail[];
|
|
21
28
|
}
|
|
22
29
|
export interface MetricsSnapshot {
|
|
23
30
|
provider?: string;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Crew Factory - Manages pool of crew instances for concurrent requests
|
|
2
|
+
|
|
3
|
+
import { AxCrew } from '../dist/index.js';
|
|
4
|
+
import type { AxCrewConfig } from '../dist/types.js';
|
|
5
|
+
import type { AxFunction } from '@ax-llm/ax';
|
|
6
|
+
|
|
7
|
+
interface CrewInstance {
|
|
8
|
+
crew: InstanceType<typeof AxCrew>;
|
|
9
|
+
busy: boolean;
|
|
10
|
+
id: number;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FactoryConfig {
|
|
15
|
+
crewConfig: AxCrewConfig;
|
|
16
|
+
crewFunctions: Record<string, AxFunction>;
|
|
17
|
+
agentNames: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MAX_POOL_SIZE = 5;
|
|
21
|
+
const INSTANCE_TTL_MS = 5 * 60 * 1000; // 5 minutes idle before cleanup
|
|
22
|
+
|
|
23
|
+
let instanceIdCounter = 0;
|
|
24
|
+
const pool: CrewInstance[] = [];
|
|
25
|
+
let isInitialized = false;
|
|
26
|
+
let factoryConfig: FactoryConfig;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the factory with at least one crew instance
|
|
30
|
+
*/
|
|
31
|
+
export async function initCrewFactory(config: FactoryConfig): Promise<void> {
|
|
32
|
+
if (isInitialized) {
|
|
33
|
+
console.log('[CrewFactory] Already initialized');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
factoryConfig = config;
|
|
38
|
+
|
|
39
|
+
console.log('[CrewFactory] Initializing...');
|
|
40
|
+
|
|
41
|
+
// Create initial instance
|
|
42
|
+
await createCrewInstance();
|
|
43
|
+
isInitialized = true;
|
|
44
|
+
|
|
45
|
+
// Start cleanup interval
|
|
46
|
+
setInterval(cleanupIdleInstances, 60000);
|
|
47
|
+
|
|
48
|
+
console.log('[CrewFactory] Initialized with 1 instance');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a new crew instance
|
|
53
|
+
*/
|
|
54
|
+
async function createCrewInstance(): Promise<CrewInstance> {
|
|
55
|
+
const id = ++instanceIdCounter;
|
|
56
|
+
console.log(`[CrewFactory] Creating instance #${id}...`);
|
|
57
|
+
|
|
58
|
+
const crew = new AxCrew(factoryConfig.crewConfig, factoryConfig.crewFunctions);
|
|
59
|
+
for (const name of factoryConfig.agentNames) {
|
|
60
|
+
await crew.addAgentsToCrew([name]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const instance: CrewInstance = {
|
|
64
|
+
crew,
|
|
65
|
+
busy: false,
|
|
66
|
+
id,
|
|
67
|
+
createdAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
pool.push(instance);
|
|
71
|
+
console.log(`[CrewFactory] Instance #${id} ready. Pool size: ${pool.length}`);
|
|
72
|
+
|
|
73
|
+
return instance;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Acquire an available crew instance, creating one if needed
|
|
78
|
+
*/
|
|
79
|
+
export async function acquireCrew(): Promise<{ crew: InstanceType<typeof AxCrew>; release: () => void }> {
|
|
80
|
+
if (!isInitialized) {
|
|
81
|
+
throw new Error('CrewFactory not initialized. Call initCrewFactory() first.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find an available instance
|
|
85
|
+
let instance = pool.find(i => !i.busy);
|
|
86
|
+
|
|
87
|
+
// If none available and pool not at max, create new instance
|
|
88
|
+
if (!instance && pool.length < MAX_POOL_SIZE) {
|
|
89
|
+
instance = await createCrewInstance();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If still none available, wait for one to become free
|
|
93
|
+
if (!instance) {
|
|
94
|
+
console.log('[CrewFactory] Pool exhausted, waiting for available instance...');
|
|
95
|
+
instance = await waitForAvailableInstance();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
instance.busy = true;
|
|
99
|
+
const instanceId = instance.id;
|
|
100
|
+
|
|
101
|
+
console.log(`[CrewFactory] Acquired instance #${instanceId}. Active: ${pool.filter(i => i.busy).length}/${pool.length}`);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
crew: instance.crew,
|
|
105
|
+
release: () => {
|
|
106
|
+
const inst = pool.find(i => i.id === instanceId);
|
|
107
|
+
if (inst) {
|
|
108
|
+
inst.busy = false;
|
|
109
|
+
inst.createdAt = Date.now(); // Reset TTL on release
|
|
110
|
+
console.log(`[CrewFactory] Released instance #${instanceId}. Active: ${pool.filter(i => i.busy).length}/${pool.length}`);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wait for an instance to become available
|
|
118
|
+
*/
|
|
119
|
+
function waitForAvailableInstance(): Promise<CrewInstance> {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const check = () => {
|
|
122
|
+
const instance = pool.find(i => !i.busy);
|
|
123
|
+
if (instance) {
|
|
124
|
+
resolve(instance);
|
|
125
|
+
} else {
|
|
126
|
+
setTimeout(check, 100);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
check();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clean up idle instances beyond the first one
|
|
135
|
+
*/
|
|
136
|
+
function cleanupIdleInstances(): void {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
const toRemove: number[] = [];
|
|
139
|
+
|
|
140
|
+
for (const instance of pool) {
|
|
141
|
+
// Keep at least one instance, only cleanup idle instances past TTL
|
|
142
|
+
if (pool.length - toRemove.length > 1 &&
|
|
143
|
+
!instance.busy &&
|
|
144
|
+
now - instance.createdAt > INSTANCE_TTL_MS) {
|
|
145
|
+
toRemove.push(instance.id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const id of toRemove) {
|
|
150
|
+
const index = pool.findIndex(i => i.id === id);
|
|
151
|
+
if (index !== -1) {
|
|
152
|
+
pool.splice(index, 1);
|
|
153
|
+
console.log(`[CrewFactory] Cleaned up idle instance #${id}. Pool size: ${pool.length}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if factory is ready
|
|
160
|
+
*/
|
|
161
|
+
export function isFactoryReady(): boolean {
|
|
162
|
+
return isInitialized;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get factory stats
|
|
167
|
+
*/
|
|
168
|
+
export function getFactoryStats() {
|
|
169
|
+
return {
|
|
170
|
+
poolSize: pool.length,
|
|
171
|
+
activeCount: pool.filter(i => i.busy).length,
|
|
172
|
+
maxPoolSize: MAX_POOL_SIZE,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Standalone test: json[] signature pattern with Google Gemini provider
|
|
2
|
+
//
|
|
3
|
+
// Tests that RoutePlanner returns waypoints as actual objects (json[]),
|
|
4
|
+
// not stringified JSON, after the Gemini schema type-union fix.
|
|
5
|
+
|
|
6
|
+
import type { AxCrewConfig } from '../dist/types.js';
|
|
7
|
+
import type { AxFunction } from '@ax-llm/ax';
|
|
8
|
+
import { FlightConstantSpeedPlanner, type LLA } from './planner.js';
|
|
9
|
+
import { initCrewFactory, acquireCrew } from './factory.js';
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Crew Configuration
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const crewConfig: AxCrewConfig = {
|
|
18
|
+
crew: [
|
|
19
|
+
{
|
|
20
|
+
name: 'RoutePlanner',
|
|
21
|
+
description: `
|
|
22
|
+
Plans optimal flight routes between geolocations.
|
|
23
|
+
Uses the PlanFlightRoute function to generate smooth paths with fly-by turns.
|
|
24
|
+
Call PlanFlightRoute with current position and destination to get detailed waypoints.
|
|
25
|
+
`,
|
|
26
|
+
signature: `
|
|
27
|
+
currentLat:number "current latitude in degrees",
|
|
28
|
+
currentLon:number "current longitude in degrees",
|
|
29
|
+
currentAlt:number "current altitude in meters",
|
|
30
|
+
heading?:number "current heading in degrees",
|
|
31
|
+
speedKts?:number "speed in knots",
|
|
32
|
+
destLat:number "destination latitude in degrees",
|
|
33
|
+
destLon:number "destination longitude in degrees",
|
|
34
|
+
destAlt:number "destination altitude in meters",
|
|
35
|
+
vehicleType?:string "type of vehicle"
|
|
36
|
+
->
|
|
37
|
+
waypoints:json[] "Array of {lat, lon, alt, heading}",
|
|
38
|
+
totalDistanceKm:number "total distance in kilometers",
|
|
39
|
+
eta:string "estimated time of arrival in MM:SS format"
|
|
40
|
+
`,
|
|
41
|
+
provider: 'openrouter' as const,
|
|
42
|
+
providerKeyName: 'OPENROUTER_API_KEY',
|
|
43
|
+
ai: {
|
|
44
|
+
model: 'stepfun/step-3.5-flash:free',
|
|
45
|
+
temperature: 0,
|
|
46
|
+
},
|
|
47
|
+
options: {
|
|
48
|
+
debug: true,
|
|
49
|
+
},
|
|
50
|
+
functions: ['PlanFlightRoute'],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Manager',
|
|
54
|
+
description: `Command center assistant that handles user requests for route planning.
|
|
55
|
+
IMPORTANT: When calling RoutePlanner, you MUST include the waypoints array it returns in your response output.`,
|
|
56
|
+
signature: `
|
|
57
|
+
userCommand:string "user request or command"
|
|
58
|
+
->
|
|
59
|
+
responseToCommand:string "A response to the command given.",
|
|
60
|
+
waypoints?:json[] "Array of {lat, lon, alt, heading} from RoutePlanner",
|
|
61
|
+
totalDistanceKm?:number "total distance in km from RoutePlanner",
|
|
62
|
+
eta?:string "ETA from RoutePlanner"
|
|
63
|
+
`,
|
|
64
|
+
provider: 'openrouter' as const,
|
|
65
|
+
providerKeyName: 'OPENROUTER_API_KEY',
|
|
66
|
+
ai: {
|
|
67
|
+
model: 'stepfun/step-3.5-flash:free',
|
|
68
|
+
temperature: 0,
|
|
69
|
+
},
|
|
70
|
+
options: {
|
|
71
|
+
debug: true,
|
|
72
|
+
},
|
|
73
|
+
agents: ['RoutePlanner'],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Haversine distance (for dynamic Hz calculation)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
const EARTH_RADIUS = 6378137.0;
|
|
83
|
+
const MAX_WAYPOINTS = 25;
|
|
84
|
+
const DEFAULT_HZ = 0.0333;
|
|
85
|
+
|
|
86
|
+
function getDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
|
87
|
+
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
|
88
|
+
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
|
89
|
+
const a =
|
|
90
|
+
Math.sin(dLat / 2) ** 2 +
|
|
91
|
+
Math.cos(lat1 * (Math.PI / 180)) *
|
|
92
|
+
Math.cos(lat2 * (Math.PI / 180)) *
|
|
93
|
+
Math.sin(dLon / 2) ** 2;
|
|
94
|
+
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * EARTH_RADIUS;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// PlanFlightRoute function (uses FlightConstantSpeedPlanner)
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
const planFlightRoute: AxFunction = {
|
|
102
|
+
name: 'PlanFlightRoute',
|
|
103
|
+
description: `Plans a flight route from current position through waypoints to destination.
|
|
104
|
+
Uses constant speed with smooth fly-by turns at 25-degree bank angle.
|
|
105
|
+
Returns detailed waypoints along the path plus mission summary (distance, time, ETA).`,
|
|
106
|
+
parameters: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
currentLat: { type: 'number', description: 'Current latitude in degrees' },
|
|
110
|
+
currentLon: { type: 'number', description: 'Current longitude in degrees' },
|
|
111
|
+
currentAlt: { type: 'number', description: 'Current altitude in meters' },
|
|
112
|
+
heading: { type: 'number', description: 'Current heading in degrees (0-360)' },
|
|
113
|
+
destLat: { type: 'number', description: 'Destination latitude in degrees' },
|
|
114
|
+
destLon: { type: 'number', description: 'Destination longitude in degrees' },
|
|
115
|
+
destAlt: { type: 'number', description: 'Destination altitude in meters' },
|
|
116
|
+
speedKts: { type: 'number', description: 'Speed in knots' },
|
|
117
|
+
},
|
|
118
|
+
required: [
|
|
119
|
+
'currentLat', 'currentLon', 'currentAlt', 'heading',
|
|
120
|
+
'destLat', 'destLon', 'destAlt', 'speedKts',
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
func: async (args) => {
|
|
124
|
+
const {
|
|
125
|
+
currentLat, currentLon, currentAlt, heading,
|
|
126
|
+
destLat, destLon, destAlt, speedKts,
|
|
127
|
+
} = args as Record<string, number>;
|
|
128
|
+
|
|
129
|
+
// Convert knots to meters per second (1 knot = 0.514444 m/s)
|
|
130
|
+
const speedMps = speedKts * 0.514444;
|
|
131
|
+
|
|
132
|
+
// Calculate dynamic Hz to limit waypoints for long routes
|
|
133
|
+
const estimatedDistanceM = getDistance(currentLat, currentLon, destLat, destLon);
|
|
134
|
+
const estimatedTimeS = estimatedDistanceM / speedMps;
|
|
135
|
+
const maxHz = MAX_WAYPOINTS / estimatedTimeS;
|
|
136
|
+
const hz = Math.min(DEFAULT_HZ, maxHz);
|
|
137
|
+
|
|
138
|
+
// Create waypoints for the planner
|
|
139
|
+
const waypoints: LLA[] = [
|
|
140
|
+
{ lat: currentLat, lon: currentLon, alt: currentAlt, heading },
|
|
141
|
+
{ lat: destLat, lon: destLon, alt: destAlt, heading: 0 },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const planner = new FlightConstantSpeedPlanner(speedMps, hz);
|
|
145
|
+
const { path, summary } = planner.getMissionDetails(waypoints);
|
|
146
|
+
|
|
147
|
+
// Return actual objects — tests that json[] works with Gemini
|
|
148
|
+
const routeWaypoints = path.map((p) => ({
|
|
149
|
+
lat: Number(p.lat.toFixed(6)),
|
|
150
|
+
lon: Number(p.lon.toFixed(6)),
|
|
151
|
+
alt: Math.round(p.alt),
|
|
152
|
+
heading: p.heading,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
waypoints: routeWaypoints,
|
|
157
|
+
totalDistanceKm: summary.totalDistanceKm,
|
|
158
|
+
totalTimeSeconds: summary.totalTimeSeconds,
|
|
159
|
+
etaFormatted: summary.etaFormatted,
|
|
160
|
+
waypointCount: routeWaypoints.length,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Function registry
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
const crewFunctions: Record<string, AxFunction> = {
|
|
170
|
+
PlanFlightRoute: planFlightRoute,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Waypoint validation helpers
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
interface Waypoint {
|
|
178
|
+
lon: number;
|
|
179
|
+
lat: number;
|
|
180
|
+
alt: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeWaypoint(wp: unknown): Waypoint | null {
|
|
184
|
+
let obj: Record<string, unknown> | null = null;
|
|
185
|
+
|
|
186
|
+
// Handle string waypoints (fallback if Gemini returns strings)
|
|
187
|
+
if (typeof wp === 'string') {
|
|
188
|
+
try {
|
|
189
|
+
obj = JSON.parse(wp);
|
|
190
|
+
} catch {
|
|
191
|
+
console.log('[Test] Failed to parse waypoint string:', wp);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
} else if (typeof wp === 'object' && wp !== null) {
|
|
195
|
+
obj = wp as Record<string, unknown>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!obj) return null;
|
|
199
|
+
|
|
200
|
+
const lon =
|
|
201
|
+
typeof obj.lon === 'number' ? obj.lon :
|
|
202
|
+
typeof obj.lng === 'number' ? obj.lng : null;
|
|
203
|
+
const lat = typeof obj.lat === 'number' ? obj.lat : null;
|
|
204
|
+
const alt =
|
|
205
|
+
typeof obj.alt === 'number' ? obj.alt :
|
|
206
|
+
typeof obj.altitude === 'number' ? obj.altitude : 1000;
|
|
207
|
+
|
|
208
|
+
if (lon === null || lat === null) return null;
|
|
209
|
+
if (lat === 0 && lon === 0) return null;
|
|
210
|
+
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
|
|
211
|
+
|
|
212
|
+
return { lon, lat, alt };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function filterValidWaypoints(waypoints: unknown[]): Waypoint[] {
|
|
216
|
+
return waypoints
|
|
217
|
+
.map(normalizeWaypoint)
|
|
218
|
+
.filter((wp): wp is Waypoint => wp !== null);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Main
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
async function main() {
|
|
226
|
+
console.log('=== Google Gemini json[] Test ===\n');
|
|
227
|
+
|
|
228
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
229
|
+
console.error('GEMINI_API_KEY not set. Add it to .env or export it.');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Initialize the factory with our config
|
|
234
|
+
await initCrewFactory({
|
|
235
|
+
crewConfig,
|
|
236
|
+
crewFunctions,
|
|
237
|
+
agentNames: ['RoutePlanner', 'Manager'],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const { crew, release } = await acquireCrew();
|
|
241
|
+
|
|
242
|
+
const manager = crew.agents?.get('Manager');
|
|
243
|
+
if (!manager) {
|
|
244
|
+
throw new Error('Manager agent not found');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
console.log('[Test] Sending route planning request...\n');
|
|
249
|
+
|
|
250
|
+
const result = await manager.forward({
|
|
251
|
+
userCommand:
|
|
252
|
+
'Plan a flight route from San Francisco (37.7749, -122.4194, alt 500m) ' +
|
|
253
|
+
'to Los Angeles (34.0522, -118.2437, alt 500m) at 250 knots, heading 150 degrees.',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.log('\n[Test] Raw Manager result:', JSON.stringify(result, null, 2));
|
|
257
|
+
|
|
258
|
+
// Validate response
|
|
259
|
+
console.log('\n--- Validation ---');
|
|
260
|
+
console.log('responseToCommand:', result.responseToCommand);
|
|
261
|
+
console.log('totalDistanceKm:', result.totalDistanceKm);
|
|
262
|
+
console.log('eta:', result.eta);
|
|
263
|
+
|
|
264
|
+
if (Array.isArray(result.waypoints)) {
|
|
265
|
+
console.log(`\nWaypoints received: ${result.waypoints.length}`);
|
|
266
|
+
|
|
267
|
+
// Check if waypoints are objects or strings
|
|
268
|
+
const first = result.waypoints[0];
|
|
269
|
+
console.log('First waypoint type:', typeof first);
|
|
270
|
+
console.log('First waypoint:', JSON.stringify(first));
|
|
271
|
+
|
|
272
|
+
const valid = filterValidWaypoints(result.waypoints);
|
|
273
|
+
console.log(`Valid waypoints: ${valid.length}/${result.waypoints.length}`);
|
|
274
|
+
|
|
275
|
+
if (valid.length > 0) {
|
|
276
|
+
console.log('\n[PASS] json[] pattern worked with Google Gemini');
|
|
277
|
+
} else {
|
|
278
|
+
console.log('\n[FAIL] Waypoints received but none were valid');
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
console.log('\n[FAIL] No waypoints array in response');
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
release();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
main().catch((err) => {
|
|
289
|
+
console.error('[FATAL]', err);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
});
|