|
import { ModuleRegistry } from "./moduleConfig" |
|
|
|
export namespace AddonDef { |
|
export type AttributeType = |
|
| "string" |
|
| "bool" |
|
| "int32" |
|
| "int64" |
|
| "Uint32" |
|
| "Uint64" |
|
| "float64" |
|
| "array" |
|
| "buf" |
|
|
|
export type Attribute = { |
|
type: AttributeType |
|
} |
|
|
|
export type PropertyDefinition = { |
|
name: string |
|
attributes: Attribute |
|
} |
|
|
|
export type Command = { |
|
name: string |
|
property?: PropertyDefinition[] |
|
required?: string[] |
|
result?: { |
|
property: PropertyDefinition[] |
|
required?: string[] |
|
} |
|
} |
|
|
|
export type ApiEndpoint = { |
|
name: string |
|
property?: PropertyDefinition[] |
|
} |
|
|
|
export type Api = { |
|
property?: Record<string, Attribute> |
|
cmd_in?: Command[] |
|
cmd_out?: Command[] |
|
data_in?: ApiEndpoint[] |
|
data_out?: ApiEndpoint[] |
|
audio_frame_in?: ApiEndpoint[] |
|
audio_frame_out?: ApiEndpoint[] |
|
video_frame_in?: ApiEndpoint[] |
|
video_frame_out?: ApiEndpoint[] |
|
} |
|
|
|
export type Module = { |
|
name: string |
|
defaultProperty: Property |
|
api: Api |
|
} |
|
} |
|
|
|
type Property = { |
|
[key: string]: any |
|
} |
|
|
|
type Node = { |
|
name: string |
|
addon: string |
|
extensionGroup: string |
|
app: string |
|
property?: Property |
|
} |
|
type Command = { |
|
name: string |
|
dest: Array<Destination> |
|
} |
|
|
|
type Data = { |
|
name: string |
|
dest: Array<Destination> |
|
} |
|
|
|
type AudioFrame = { |
|
name: string |
|
dest: Array<Destination> |
|
} |
|
|
|
type VideoFrame = { |
|
name: string |
|
dest: Array<Destination> |
|
} |
|
|
|
type MsgConversion = { |
|
type: string |
|
rules: Array<{ |
|
path: string |
|
conversionMode: string |
|
value?: string |
|
originalPath?: string |
|
}> |
|
keepOriginal?: boolean |
|
} |
|
|
|
type Destination = { |
|
app: string |
|
extension: string |
|
msgConversion?: MsgConversion |
|
} |
|
|
|
type Connection = { |
|
app: string |
|
extension: string |
|
cmd?: Array<Command> |
|
data?: Array<Data> |
|
audio_frame?: Array<AudioFrame> |
|
video_frame?: Array<VideoFrame> |
|
} |
|
|
|
type Graph = { |
|
id: string |
|
autoStart: boolean |
|
nodes: Node[] |
|
connections: Connection[] |
|
} |
|
|
|
enum GraphConnProtocol { |
|
CMD = "cmd", |
|
DATA = "data", |
|
AUDIO_FRAME = "audio_frame", |
|
VIDEO_FRAME = "video_frame", |
|
} |
|
|
|
class GraphEditor { |
|
private static sharedApp: string = "localhost" |
|
|
|
|
|
|
|
|
|
static setApp(app: string): void { |
|
GraphEditor.sharedApp = app |
|
} |
|
|
|
|
|
|
|
|
|
static addNode( |
|
graph: Graph, |
|
addon: string, |
|
name: string, |
|
group: string = "default", |
|
properties: Record<string, any> = {}, |
|
): Node { |
|
if (graph.nodes.some((node) => node.name === name)) { |
|
throw new Error( |
|
`Node with name "${name}" already exists in graph "${graph.id}".`, |
|
) |
|
} |
|
|
|
const node: Node = { |
|
name, |
|
addon, |
|
extensionGroup: group, |
|
app: GraphEditor.sharedApp, |
|
property: properties, |
|
} |
|
|
|
graph.nodes.push(node) |
|
return node |
|
} |
|
|
|
|
|
|
|
|
|
static removeNode(graph: Graph, nodeName: string): Node { |
|
const nodeIndex = graph.nodes.findIndex((node) => node.name === nodeName) |
|
if (nodeIndex === -1) { |
|
throw new Error(`Node "${nodeName}" not found in graph "${graph.id}".`) |
|
} |
|
const node = graph.nodes.splice(nodeIndex, 1)[0] |
|
return node; |
|
} |
|
|
|
|
|
|
|
|
|
static updateNode( |
|
graph: Graph, |
|
nodeName: string, |
|
properties: Record<string, any>, |
|
): void { |
|
const node = graph.nodes.find((node) => node.name === nodeName) |
|
if (!node) { |
|
throw new Error(`Node "${nodeName}" not found in graph "${graph.id}".`) |
|
} |
|
|
|
|
|
node.property = { |
|
...node.property, |
|
...Object.fromEntries( |
|
Object.entries(properties).filter(([_, value]) => value !== ""), |
|
), |
|
} |
|
} |
|
|
|
static updateNodeProperty( |
|
graph: Graph, |
|
nodeName: string, |
|
properties: Record<string, any>, |
|
): boolean { |
|
const node = this.findNode(graph, nodeName) |
|
if (!node) return false |
|
|
|
node.property = { |
|
...node.property, |
|
...Object.fromEntries( |
|
Object.entries(properties).filter(([_, value]) => value !== ""), |
|
), |
|
} |
|
|
|
return true |
|
} |
|
|
|
static removeNodeProperties( |
|
graph: Graph, |
|
nodeName: string, |
|
properties: string[], |
|
): boolean { |
|
const node = this.findNode(graph, nodeName) |
|
if (!node) return false |
|
|
|
properties.forEach((prop) => { |
|
if (node.property) delete node.property[prop] |
|
}) |
|
|
|
return true |
|
} |
|
|
|
|
|
|
|
|
|
static addConnection( |
|
graph: Graph, |
|
source: string, |
|
destination: string, |
|
protocolLabel: GraphConnProtocol, |
|
protocolName: string, |
|
): void { |
|
|
|
let connection = graph.connections.find( |
|
(conn) => |
|
conn.extension === source, |
|
) |
|
|
|
if (!connection) { |
|
|
|
connection = { |
|
app: GraphEditor.sharedApp, |
|
extension: source, |
|
} |
|
graph.connections.push(connection) |
|
} |
|
|
|
|
|
const protocolField = protocolLabel.toLowerCase() as keyof Connection |
|
if (!connection[protocolField]) { |
|
connection[protocolField] = [] as any |
|
} |
|
|
|
const protocolArray = connection[protocolField] as Array< |
|
Command | Data | AudioFrame | VideoFrame |
|
> |
|
|
|
|
|
let protocolObject = protocolArray.find( |
|
(item) => item.name === protocolName, |
|
) |
|
if (!protocolObject) { |
|
protocolObject = { |
|
name: protocolName, |
|
dest: [], |
|
} |
|
protocolArray.push(protocolObject) |
|
} |
|
|
|
|
|
if ( |
|
protocolObject.dest.some( |
|
(dest) => dest.extension === destination, |
|
) |
|
) { |
|
throw new Error( |
|
`Destination "${destination}" already exists in protocol "${protocolLabel}" with name "${protocolName}".`, |
|
) |
|
} |
|
|
|
|
|
protocolObject.dest.push({ |
|
app: GraphEditor.sharedApp, |
|
extension: destination, |
|
}) |
|
} |
|
static addOrUpdateConnection( |
|
graph: Graph, |
|
source: string, |
|
destination: string, |
|
protocolLabel: GraphConnProtocol, |
|
protocolName: string, |
|
): void { |
|
let connection = this.findConnection(graph, source) |
|
|
|
if (connection) { |
|
|
|
const protocolField = protocolLabel.toLowerCase() as keyof Connection |
|
if (!connection[protocolField]) { |
|
connection[protocolField] = [] as any |
|
} |
|
|
|
const protocolArray = connection[protocolField] as Array< |
|
Command | Data | AudioFrame | VideoFrame |
|
> |
|
|
|
|
|
let protocolObject = protocolArray.find( |
|
(item) => item.name === protocolName, |
|
) |
|
if (!protocolObject) { |
|
|
|
protocolObject = { |
|
name: protocolName, |
|
dest: [], |
|
} |
|
protocolArray.push(protocolObject) |
|
} |
|
|
|
|
|
if ( |
|
!protocolObject.dest.some( |
|
(dest) => dest.extension === destination, |
|
) |
|
) { |
|
|
|
protocolObject.dest.push({ |
|
app: GraphEditor.sharedApp, |
|
extension: destination, |
|
}) |
|
} |
|
} else { |
|
|
|
this.addConnection( |
|
graph, |
|
source, |
|
destination, |
|
protocolLabel, |
|
protocolName, |
|
) |
|
} |
|
} |
|
|
|
|
|
|
|
static removeConnection( |
|
graph: Graph, |
|
source: string, |
|
destination?: string, |
|
protocolLabel?: GraphConnProtocol, |
|
protocolName?: string, |
|
): void { |
|
|
|
const connectionIndex = graph.connections.findIndex( |
|
(conn) => |
|
conn.extension === source, |
|
); |
|
|
|
if (connectionIndex === -1) { |
|
console.warn(`Source "${source}" not found in the graph. Operation ignored.`); |
|
return; |
|
} |
|
|
|
const connection = graph.connections[connectionIndex]; |
|
|
|
|
|
if (protocolLabel) { |
|
const protocolField = protocolLabel.toLowerCase() as keyof Connection; |
|
const protocolArray = connection[protocolField] as Array< |
|
Command | Data | AudioFrame | VideoFrame |
|
>; |
|
|
|
if (!protocolArray) { |
|
console.warn( |
|
`Protocol "${protocolLabel}" does not exist for source "${source}". Operation ignored.` |
|
); |
|
return; |
|
} |
|
|
|
const protocolObjectIndex = protocolArray.findIndex( |
|
(item) => item.name === protocolName, |
|
); |
|
|
|
if (protocolObjectIndex === -1) { |
|
console.warn( |
|
`Protocol object with name "${protocolName}" not found in protocol "${protocolLabel}". Operation ignored.` |
|
); |
|
return; |
|
} |
|
|
|
if (destination) { |
|
|
|
protocolArray[protocolObjectIndex].dest = protocolArray[ |
|
protocolObjectIndex |
|
].dest.filter((dest) => dest.extension !== destination); |
|
|
|
|
|
if (protocolArray[protocolObjectIndex].dest.length === 0) { |
|
protocolArray.splice(protocolObjectIndex, 1); |
|
} |
|
} else { |
|
|
|
protocolArray.splice(protocolObjectIndex, 1); |
|
} |
|
} else { |
|
|
|
graph.connections.splice(connectionIndex, 1); |
|
} |
|
|
|
|
|
GraphEditor.removeEmptyConnections(graph); |
|
} |
|
|
|
static findNode(graph: Graph, nodeName: string): Node | null { |
|
return graph.nodes.find((node) => node.name === nodeName) || null |
|
} |
|
|
|
static findNodeByPredicate(graph: Graph, predicate: (node: Node) => boolean): Node | null { |
|
return graph.nodes.find(predicate) || null |
|
} |
|
|
|
static findConnection( |
|
graph: Graph, |
|
extension: string, |
|
): Connection | null { |
|
return ( |
|
graph.connections.find( |
|
(conn) => |
|
conn.extension === extension, |
|
) || null |
|
) |
|
} |
|
|
|
static removeEmptyConnections(graph: Graph): void { |
|
graph.connections = graph.connections.filter((connection) => { |
|
|
|
connection.cmd = Array.isArray(connection.cmd) |
|
? connection.cmd.filter((cmd) => cmd.dest?.length > 0) |
|
: undefined; |
|
if (!connection.cmd?.length) delete connection.cmd; |
|
|
|
connection.data = Array.isArray(connection.data) |
|
? connection.data.filter((data) => data.dest?.length > 0) |
|
: undefined; |
|
if (!connection.data?.length) delete connection.data; |
|
|
|
connection.audio_frame = Array.isArray(connection.audio_frame) |
|
? connection.audio_frame.filter((audio) => audio.dest?.length > 0) |
|
: undefined; |
|
if (!connection.audio_frame?.length) delete connection.audio_frame; |
|
|
|
connection.video_frame = Array.isArray(connection.video_frame) |
|
? connection.video_frame.filter((video) => video.dest?.length > 0) |
|
: undefined; |
|
if (!connection.video_frame?.length) delete connection.video_frame; |
|
|
|
|
|
return ( |
|
connection.cmd?.length || |
|
connection.data?.length || |
|
connection.audio_frame?.length || |
|
connection.video_frame?.length |
|
); |
|
}); |
|
} |
|
|
|
|
|
|
|
static removeNodeAndConnections(graph: Graph, addon: string): void { |
|
|
|
const node = this.removeNode(graph, addon) |
|
|
|
|
|
graph.connections = graph.connections.filter((connection) => { |
|
const isSource = |
|
connection.extension === `${node.name}` |
|
const protocols = ["cmd", "data", "audio_frame", "video_frame"] as const |
|
|
|
protocols.forEach((protocol) => { |
|
if (connection[protocol]) { |
|
connection[protocol].forEach((item) => item.dest = item.dest.filter(d => d.extension !== node.name), |
|
) |
|
} |
|
}) |
|
|
|
|
|
return ( |
|
!isSource && |
|
(connection.cmd?.length || |
|
connection.data?.length || |
|
connection.audio_frame?.length || |
|
connection.video_frame?.length) |
|
) |
|
}) |
|
|
|
GraphEditor.removeEmptyConnections(graph); |
|
} |
|
|
|
|
|
|
|
|
|
static linkTool(graph: Graph, llmNode: Node, toolNode: Node, tool: ModuleRegistry.ToolModule): void { |
|
|
|
GraphEditor.addOrUpdateConnection( |
|
graph, |
|
`${llmNode.name}`, |
|
`${toolNode.name}`, |
|
GraphConnProtocol.CMD, |
|
"tool_call" |
|
); |
|
|
|
|
|
GraphEditor.addOrUpdateConnection( |
|
graph, |
|
`${toolNode.name}`, |
|
`${llmNode.name}`, |
|
GraphConnProtocol.CMD, |
|
"tool_register" |
|
); |
|
|
|
const rtcModule = GraphEditor.findNodeByPredicate(graph, (node) => node.addon.includes("rtc")); |
|
if (toolNode.addon.includes("vision") && rtcModule) { |
|
|
|
GraphEditor.addOrUpdateConnection( |
|
graph, |
|
`${rtcModule.name}`, |
|
`${toolNode.name}`, |
|
GraphConnProtocol.VIDEO_FRAME, |
|
"video_frame" |
|
); |
|
} |
|
|
|
const messageCollector = GraphEditor.findNodeByPredicate(graph, ((node) => node.addon.includes("message_collector"))) |
|
if (tool.options.outputContentText && messageCollector) { |
|
GraphEditor.addOrUpdateConnection( |
|
graph, |
|
`${toolNode.name}`, |
|
`${messageCollector.name}`, |
|
GraphConnProtocol.DATA, |
|
"content_data" |
|
) |
|
} |
|
} |
|
|
|
static enableRTCVideoSubscribe(graph: Graph, enabled: Boolean): void { |
|
const rtcNode = GraphEditor.findNodeByPredicate(graph, (node) => node.addon.includes("rtc")); |
|
if (!rtcNode) { |
|
throw new Error("RTC node not found in the graph."); |
|
} |
|
|
|
if (enabled) { |
|
GraphEditor.updateNodeProperty(graph, rtcNode.name, { |
|
subscribe_video_pix_fmt: 4, |
|
subscribe_video: true, |
|
}); |
|
} else { |
|
GraphEditor.removeNodeProperties(graph, rtcNode.name, ["subscribe_video_pix_fmt", "subscribe_video"]); |
|
} |
|
} |
|
} |
|
|
|
export type { Graph, Node, Connection, Command, Destination } |
|
|
|
export { GraphEditor, GraphConnProtocol as ProtocolLabel } |
|
|