Skip to content

Commit d2d1ef5

Browse files
committed
add agent components
1 parent f54b7ba commit d2d1ef5

14 files changed

Lines changed: 721 additions & 287 deletions

File tree

package-lock.json

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

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@
3434
"@vscode/codicons": "^0.0.36",
3535
"bootstrap": "^5.3.0",
3636
"bootstrap-icons": "^1.10.5",
37-
"fusio-sdk": "^6.1.0",
38-
"marked": "^17.0.0",
37+
"emoji-toolkit": "^10.0.0",
38+
"fusio-sdk": "^7.0.8",
39+
"katex": "^0.16.47",
40+
"marked": "^18.0.0",
3941
"ngx-captcha": "^14.0.0",
4042
"ngx-clipboard": "^16.0.0",
4143
"ngx-gravatar": "^13.0.0",
42-
"ngx-markdown": "^21.1.0",
44+
"ngx-markdown": "^21.3.0",
4345
"ngx-typeschema-editor": "^5.0.0",
4446
"rxjs": "~7.8.0",
4547
"tslib": "^2.3.0"
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
AgentInput,
3+
AgentItem,
4+
AgentItemBinary,
5+
AgentItemChoice,
6+
AgentItemObject,
7+
AgentItemText,
8+
AgentItemToolCall,
9+
AgentOutput,
10+
CommonMessage
11+
} from "fusio-sdk";
12+
import {inject} from "@angular/core";
13+
import {FusioService} from "../service/fusio.service";
14+
15+
export interface Agent<TModel, TOptions = undefined> {
16+
17+
/**
18+
* Sends a prompt to a specific agent and returns the content
19+
*/
20+
prompt(agentId: number, prompt: string, chatId?: string): Promise<AgentItem|undefined>;
21+
22+
/**
23+
* Transforms the agent content into a model
24+
*/
25+
transform(content: BackendAgentContent): TModel|undefined;
26+
27+
/**
28+
* Executes the provided model, mostly this means that we create or update the model
29+
*/
30+
execute(model: TModel, indicator: ExecutionIndicator, options?: TOptions): Promise<CommonMessage|undefined>;
31+
32+
}
33+
34+
export abstract class AgentAbstract<TModel, TOptions = undefined> implements Agent<TModel, TOptions> {
35+
36+
protected api = inject(FusioService);
37+
38+
async prompt(agentId: number, prompt: string, chatId?: string): Promise<BackendAgentContent|undefined> {
39+
const input: AgentInput = {
40+
previousId: chatId,
41+
item: {
42+
type: 'text',
43+
content: prompt,
44+
}
45+
};
46+
47+
const output = await this.submit(agentId, input);
48+
if (!output.item) {
49+
return;
50+
}
51+
52+
return output.item;
53+
}
54+
55+
abstract submit(agentId: number, input: AgentInput): Promise<AgentOutput>;
56+
57+
abstract transform(content: BackendAgentContent): TModel|undefined;
58+
59+
abstract execute(model: TModel, indicator: ExecutionIndicator, options?: TOptions): Promise<CommonMessage|undefined>;
60+
61+
protected getText(content: BackendAgentContent): string|undefined {
62+
if (content.type === 'text' && content.content) {
63+
return content.content;
64+
}
65+
66+
return;
67+
}
68+
69+
protected getJson(content: BackendAgentContent): object|undefined {
70+
if (content.type === 'object' && content.payload) {
71+
return content.payload;
72+
}
73+
74+
return;
75+
}
76+
77+
}
78+
79+
export class ExecutionIndicator {
80+
81+
constructor(private callback: Function) {
82+
}
83+
84+
request(message: string) {
85+
const result: Message = {
86+
level: 'info',
87+
message: '> ' + message,
88+
};
89+
90+
this.callback.apply(null, [result]);
91+
}
92+
93+
response(message?: CommonMessage) {
94+
if (!message || !message.message) {
95+
return;
96+
}
97+
98+
let result: Message|undefined = undefined;
99+
if (message.success === true) {
100+
result = {
101+
level: 'success',
102+
message: '< ' + message.message,
103+
};
104+
} else if (message.success === false) {
105+
result = {
106+
level: 'danger',
107+
message: '< ' + message.message,
108+
};
109+
}
110+
111+
if (result !== undefined) {
112+
this.callback.apply(null, [result]);
113+
}
114+
}
115+
116+
}
117+
118+
export interface Message
119+
{
120+
level: Level,
121+
message: string,
122+
}
123+
124+
export type Level = 'info'|'danger'|'success';
125+
126+
export type BackendAgentContent = AgentItemBinary | AgentItemChoice | AgentItemObject | AgentItemText | AgentItemToolCall;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {Component, computed, inject, input, resource, signal} from '@angular/core';
2+
import {AgentItem, BackendAgent, BackendAgentMessage, CommonMessage} from "fusio-sdk";
3+
import {Agent, BackendAgentContent, ExecutionIndicator, Message} from "../../abstract/agent";
4+
import {FusioService} from "../../service/fusio.service";
5+
import {ErrorService} from "../../service/error.service";
6+
7+
@Component({
8+
selector: 'fusio-agent-chat-abstract',
9+
template: ''
10+
})
11+
export abstract class ChatAbstract<TModel, TOptions = undefined> {
12+
13+
agent = input.required<BackendAgent>();
14+
chatId = input.required<string>();
15+
16+
model = signal<TModel|undefined>(undefined);
17+
18+
output = signal<AgentItem|undefined>(undefined);
19+
loading = signal<boolean>(false);
20+
executeLoading = signal<boolean>(false);
21+
executeMessages = signal<Array<Message>>([]);
22+
response = signal<CommonMessage|undefined>(undefined);
23+
24+
messagesResource = resource<Array<BackendAgentMessage>, { agent: BackendAgent, chatId: string, output: AgentItem|undefined }>({
25+
params: () => ({
26+
agent: this.agent(),
27+
chatId: this.chatId(),
28+
output: this.output(),
29+
}),
30+
loader: async (params) => {
31+
const collection = await this.api.getClient().backend().agent().message().getAll('' + params.params.agent.id, params.params.chatId);
32+
const entries = collection.entry || [];
33+
34+
let lastMessage: BackendAgentMessage|undefined;
35+
const messages: Array<BackendAgentMessage> = [];
36+
entries.forEach((message) => {
37+
messages.push(message);
38+
39+
if (message.role === 'assistant') {
40+
lastMessage = message;
41+
}
42+
});
43+
44+
if (lastMessage && lastMessage.item) {
45+
this.load(lastMessage.item);
46+
}
47+
48+
return messages;
49+
}
50+
});
51+
52+
messages = computed<Array<BackendAgentMessage>|undefined>(() => {
53+
if (this.messagesResource.hasValue()) {
54+
return this.messagesResource.value();
55+
}
56+
57+
return undefined;
58+
});
59+
60+
protected api = inject(FusioService);
61+
protected error = inject(ErrorService);
62+
63+
abstract getAgent(): Agent<TModel, TOptions>;
64+
65+
async doSend(message: string) {
66+
if (!message) {
67+
return;
68+
}
69+
70+
const agentId = this.agent().id;
71+
if (!agentId) {
72+
return;
73+
}
74+
75+
this.loading.set(true);
76+
77+
try {
78+
const content = await this.getAgent().prompt(agentId, message, this.chatId());
79+
80+
this.output.set(content);
81+
82+
this.onSend();
83+
84+
this.scrollToBottom();
85+
} catch (error) {
86+
this.response.set(this.error.convert(error));
87+
}
88+
89+
this.loading.set(false);
90+
}
91+
92+
load(content?: BackendAgentContent): void {
93+
if (!content) {
94+
return;
95+
}
96+
97+
this.executeMessages.set([]);
98+
this.model.set(this.getAgent().transform(content));
99+
100+
this.onLoad();
101+
}
102+
103+
async execute(): Promise<void> {
104+
const model = this.model();
105+
if (!model) {
106+
return;
107+
}
108+
109+
this.executeLoading.set(true);
110+
111+
try {
112+
const executeMessages = this.executeMessages;
113+
const indicator = new ExecutionIndicator((message: Message) => {
114+
executeMessages.update((messages) => {
115+
return messages.concat([message]);
116+
});
117+
});
118+
119+
const options = this.getOptions();
120+
121+
const response = await this.getAgent().execute(model, indicator, options);
122+
this.response.set(response);
123+
124+
if (response) {
125+
this.onExecute(response);
126+
}
127+
} catch (error) {
128+
this.response.set(this.error.convert(error));
129+
}
130+
131+
this.executeLoading.set(false);
132+
}
133+
134+
protected onLoad(): void {
135+
}
136+
137+
protected onSend(): void {
138+
}
139+
140+
protected onExecute(message: CommonMessage): void {
141+
}
142+
143+
protected getOptions(): TOptions|undefined {
144+
return;
145+
}
146+
147+
scrollToBottom(): void {
148+
window.setTimeout(() => {
149+
let messagesBottom = document.getElementById('messages-bottom');
150+
if (messagesBottom !== null) {
151+
messagesBottom.scrollIntoView();
152+
}
153+
}, 500);
154+
}
155+
156+
}

projects/fusio-sdk/src/lib/component/agent/input/input.css

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
<div class="sticky-bottom p-2 mt-3 bg-light">
3+
@if (loading()) {
4+
<div class="spinner-grow mb-3" role="status">
5+
<span class="visually-hidden">Loading...</span>
6+
</div>
7+
}
8+
<form>
9+
<div class="mb-3">
10+
<textarea id="input" name="input" [ngModel]="input()" (ngModelChange)="input.set($event)" placeholder="Send a message ..." class="form-control" rows="6"></textarea>
11+
</div>
12+
<div class="btn-group">
13+
<button class="btn btn-primary" (click)="send.emit(input());input.set('');">Send</button>
14+
</div>
15+
</form>
16+
</div>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {Component, input, OnInit, output, signal} from '@angular/core';
2+
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
3+
4+
@Component({
5+
selector: 'fusio-agent-message-input',
6+
imports: [
7+
FormsModule,
8+
ReactiveFormsModule
9+
],
10+
templateUrl: './input.html',
11+
styleUrl: './input.css',
12+
})
13+
export class Input implements OnInit {
14+
15+
loading = input.required<boolean>();
16+
text = input<string>('');
17+
18+
input = signal<string>('');
19+
send = output<string>();
20+
21+
ngOnInit(): void {
22+
this.input.set(this.text());
23+
}
24+
25+
}

projects/fusio-sdk/src/lib/component/agent/row/row.css

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
@if (message(); as message) {
3+
<div class="mb-2 overflow-hidden" [ngClass]="{'bg-light p-3 rounded-4': message.role === 'user'}">
4+
@if (message.item) {
5+
@if (message.item.type === 'text' && 'content' in message.item && message.item.content) {
6+
@if (message.role === 'user') {
7+
{{ message.item.content }}
8+
}
9+
@else {
10+
<markdown emoji katex [katexOptions]="katexOptions" [data]="message.item.content"></markdown>
11+
}
12+
}
13+
@else if (message.item.type === 'object' && 'payload' in message.item && message.item.payload) {
14+
<markdown>```json
15+
{{ message.item.payload|json }}
16+
```</markdown>
17+
}
18+
@else {
19+
<markdown>```json
20+
{{ message.item|json }}
21+
```</markdown>
22+
}
23+
}
24+
</div>
25+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {Component, input} from '@angular/core';
2+
import {JsonPipe, NgClass} from "@angular/common";
3+
import {BackendAgentMessage} from "fusio-sdk";
4+
import {KatexSpecificOptions, MarkdownComponent} from "ngx-markdown";
5+
6+
@Component({
7+
selector: 'fusio-agent-message-row',
8+
imports: [
9+
JsonPipe,
10+
NgClass,
11+
MarkdownComponent
12+
],
13+
templateUrl: './row.html',
14+
styleUrl: './row.css',
15+
})
16+
export class Row {
17+
18+
message = input.required<BackendAgentMessage>();
19+
20+
katexOptions: KatexSpecificOptions = {
21+
throwOnError: false,
22+
};
23+
24+
}

0 commit comments

Comments
 (0)