@hashbrownai/angular
Version:
Angular bindings for Hashbrown AI
679 lines (658 loc) • 23.5 kB
JavaScript
import * as i0 from '@angular/core';
import { Injector, inject, runInInjectionContext, untracked, reflectComponentType, DestroyRef, signal, input, ApplicationRef, computed, ViewContainerRef, Component, InjectionToken, effect } from '@angular/core';
import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { ɵcreateRuntimeFunctionImpl as _createRuntimeFunctionImpl, ɵcreateRuntimeImpl as _createRuntimeImpl, s, fryHashbrown, ɵcomponents as _components } from '@hashbrownai/core';
/* eslint-disable @typescript-eslint/no-explicit-any */
function createRuntimeFunction(cfg) {
const injector = inject(Injector);
return _createRuntimeFunctionImpl({
...cfg,
handler: (...args) => {
return runInInjectionContext(injector, () => cfg.handler(...args));
},
});
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates a new runtime.
*
* @param options - The options for creating the runtime.
* @returns A reference to the runtime.
*/
function createRuntime(options) {
return _createRuntimeImpl(options);
}
function createTool(input) {
const injector = inject(Injector);
if ('schema' in input) {
const { name, description, schema, handler } = input;
return {
name,
description,
schema,
handler: (args, abortSignal) => untracked(() => runInInjectionContext(injector, () => handler(args, abortSignal))),
};
}
else {
const { name, description, handler } = input;
return {
name,
description,
schema: s.object('Empty Object', {}),
handler: (_, abortSignal) => untracked(() => runInInjectionContext(injector, () => handler(abortSignal))),
};
}
}
/**
* Creates a tool that allows the LLM to run JavaScript code. It is run
* in a stateful JavaScript environment, with no access to the internet, the DOM,
* or any function that you have not explicitly defined.
*
* @param options - The options for creating the tool.
* @returns The tool.
*/
function createToolJavaScript({ runtime }) {
return createTool({
name: 'javascript',
description: [
'Whenever you send a message containing JavaScript code to javascript, it will be',
'executed in a stateful JavaScript environment. javascript will respond with the output',
'of the execution or time out after ${runtime.timeout / 1000} seconds. Internet access',
'for this session is disabled. Do not make external web requests or API calls as they',
'will fail.',
'',
'Important: Prefer calling javascript once with a large amount of code, rather than calling it',
'multiple times with smaller amounts of code.',
'',
'The following functions are available to you:',
runtime.describe(),
].join('\n'),
schema: s.streaming.object('The result', {
code: s.streaming.string('The JavaScript code to run'),
}),
handler: async ({ code }, abortSignal) => {
return runtime.run(code, abortSignal);
},
});
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Exposes a component by combining it with additional configuration details.
*
* @template T - The type of the Angular component.
* @param {T} component - The Angular component to be exposed.
* @param {Prettify<Omit<ExposedComponent<T>, 'component' | 'name'>>} config - The configuration object for the component, excluding the component itself.
* @returns {ExposedComponent<T>} - An object representing the exposed component, including the component and its configuration.
*/
function exposeComponent(component, config) {
const reflected = reflectComponentType(component);
if (!reflected?.selector) {
throw new Error(`Could not reflect component: ${component}`);
}
const { input, ...rest } = config;
return {
component,
...rest,
props: input,
name: reflected?.selector,
};
}
function readSignalLike(signalLike) {
if (typeof signalLike === 'function') {
return signalLike();
}
return signalLike;
}
function toNgSignal(source, debugName) {
const destroyRef = inject(DestroyRef);
const _signal = signal(source(), {
debugName,
});
const teardown = source.subscribe((value) => {
_signal.set(value);
});
destroyRef.onDestroy(() => {
teardown();
});
return _signal.asReadonly();
}
const TAG_NAME_REGISTRY = Symbol('ɵtagNameRegistry');
const getTagNameRegistry = (message) => {
if (TAG_NAME_REGISTRY in message) {
return message[TAG_NAME_REGISTRY];
}
return undefined;
};
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @angular-eslint/component-selector */
/**
* Renders messages generated by the assistant from uiChatResource.
*
* @example
*
* ```html
* <hb-render-message [message]="message" />
* ```
*/
class RenderMessageComponent {
message = input.required();
/**
* @internal
*/
appRef = inject(ApplicationRef);
/**
* @internal
*/
content = computed(() => this.message().content?.ui ?? []);
/**
* @internal
*/
tagNameRegistry = computed(() => getTagNameRegistry(this.message()));
/**
* @internal
*/
viewContainerRef = inject(ViewContainerRef);
/**
* @internal
*/
rootNodesWeakMap = new WeakMap();
/**
* @internal
*/
embeddedViewsWeakMap = new WeakMap();
/**
* @internal
*/
getTagComponent(tagName) {
return this.tagNameRegistry()?.[tagName]?.component ?? null;
}
/**
* @internal
*/
getEmbeddedView(tpl) {
if (this.embeddedViewsWeakMap.has(tpl)) {
return this.embeddedViewsWeakMap.get(tpl);
}
const view = this.viewContainerRef.createEmbeddedView(tpl);
this.embeddedViewsWeakMap.set(tpl, view);
return view;
}
/**
* @internal
*/
getRootNodes(tpl) {
if (this.rootNodesWeakMap.has(tpl)) {
return this.rootNodesWeakMap.get(tpl);
}
const view = this.getEmbeddedView(tpl);
const nodes = [view.rootNodes];
this.rootNodesWeakMap.set(tpl, nodes);
return nodes;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: RenderMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.5", type: RenderMessageComponent, isStandalone: true, selector: "hb-render-message", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
<ng-template #nodeTemplateRef let-node="node">
<ng-template #childrenTemplateRef>
@for (child of node.$children; track $index) {
<ng-container
*ngTemplateOutlet="nodeTemplateRef; context: { node: child }"
/>
}
</ng-template>
@if (node) {
<ng-container
*ngComponentOutlet="
getTagComponent(node.$tagName);
inputs: node.$props;
content: getRootNodes(childrenTemplateRef)
"
></ng-container>
}
</ng-template>
@if (content()) {
@for (node of content(); track $index) {
<ng-template
[ngTemplateOutlet]="nodeTemplateRef"
[ngTemplateOutletContext]="node"
>
</ng-template>
<ng-container
*ngTemplateOutlet="nodeTemplateRef; context: { node: node }"
/>
}
}
`, isInline: true, dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: RenderMessageComponent, decorators: [{
type: Component,
args: [{
selector: 'hb-render-message',
imports: [NgComponentOutlet, NgTemplateOutlet],
template: `
<ng-template #nodeTemplateRef let-node="node">
<ng-template #childrenTemplateRef>
@for (child of node.$children; track $index) {
<ng-container
*ngTemplateOutlet="nodeTemplateRef; context: { node: child }"
/>
}
</ng-template>
@if (node) {
<ng-container
*ngComponentOutlet="
getTagComponent(node.$tagName);
inputs: node.$props;
content: getRootNodes(childrenTemplateRef)
"
></ng-container>
}
</ng-template>
@if (content()) {
@for (node of content(); track $index) {
<ng-template
[ngTemplateOutlet]="nodeTemplateRef"
[ngTemplateOutletContext]="node"
>
</ng-template>
<ng-container
*ngTemplateOutlet="nodeTemplateRef; context: { node: node }"
/>
}
}
`,
}]
}] });
/**
* @internal
*/
const ɵHASHBROWN_CONFIG_INJECTION_TOKEN = new InjectionToken('HashbrownConfig');
/**
* Provides the Hashbrown configuration.
*
* @argument options - The Hashbrown configuration.
* @returns The Hashbrown configuration.
*/
function provideHashbrown(options) {
return {
provide: ɵHASHBROWN_CONFIG_INJECTION_TOKEN,
useValue: options,
};
}
/**
* @internal
*/
function ɵinjectHashbrownConfig() {
return inject(ɵHASHBROWN_CONFIG_INJECTION_TOKEN);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates a chat resource for managing LLM-driven conversations.
*
* @template Tools
* @param {ChatResourceOptions<Tools>} options - Configuration for the chat resource.
* @returns {ChatResourceRef<Tools>} An object with reactive signals and methods for interacting with the chat.
*/
function chatResource(options) {
const config = ɵinjectHashbrownConfig();
const injector = inject(Injector);
const destroyRef = inject(DestroyRef);
const hashbrown = fryHashbrown({
apiUrl: options.apiUrl ?? config.baseUrl,
middleware: config.middleware?.map((m) => {
return (requestInit) => runInInjectionContext(injector, () => m(requestInit));
}),
system: readSignalLike(options.system),
model: readSignalLike(options.model),
tools: options.tools,
emulateStructuredOutput: config.emulateStructuredOutput,
debugName: options.debugName,
});
const teardown = hashbrown.sizzle();
destroyRef.onDestroy(() => teardown());
const value = toNgSignal(hashbrown.messages, options.debugName && `${options.debugName}.value`);
const isReceiving = toNgSignal(hashbrown.isReceiving, options.debugName && `${options.debugName}.isReceiving`);
const isSending = toNgSignal(hashbrown.isSending, options.debugName && `${options.debugName}.isSending`);
const isRunningToolCalls = toNgSignal(hashbrown.isRunningToolCalls, options.debugName && `${options.debugName}.isRunningToolCalls`);
const error = toNgSignal(hashbrown.error, options.debugName && `${options.debugName}.error`);
const lastAssistantMessage = toNgSignal(hashbrown.lastAssistantMessage, options.debugName && `${options.debugName}.lastAssistantMessage`);
const status = computed(() => {
if (isReceiving() || isSending() || isRunningToolCalls()) {
return 'loading';
}
if (error()) {
return 'error';
}
const hasAssistantMessage = value().some((message) => message.role === 'assistant');
if (hasAssistantMessage) {
return 'resolved';
}
return 'idle';
}, { debugName: options.debugName && `${options.debugName}.status` });
const isLoading = computed(() => {
return isReceiving() || isSending() || isRunningToolCalls();
}, { debugName: options.debugName && `${options.debugName}.isLoading` });
function reload() {
const lastMessage = value()[value().length - 1];
if (lastMessage.role === 'assistant') {
hashbrown.setMessages(value().slice(0, -1));
return true;
}
return false;
}
function hasValue() {
return value().some((message) => message.role === 'assistant');
}
function sendMessage(message) {
hashbrown.sendMessage(message);
}
function stop(clearStreamingMessage = false) {
hashbrown.stop(clearStreamingMessage);
}
return {
hasValue: hasValue,
status,
isLoading,
reload,
sendMessage,
stop,
value,
error,
lastAssistantMessage,
};
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates a completion resource.
*
* @param options - The options for the completion resource.
* @returns The completion resource.
*/
function completionResource(options) {
const { model, input, system } = options;
const injector = inject(Injector);
const destroyRef = inject(DestroyRef);
const config = ɵinjectHashbrownConfig();
const hashbrown = fryHashbrown({
debugName: options.debugName,
apiUrl: options.apiUrl ?? config.baseUrl,
middleware: config.middleware?.map((m) => {
return (requestInit) => runInInjectionContext(injector, () => m(requestInit));
}),
model: readSignalLike(model),
system: readSignalLike(system),
messages: [],
tools: [],
retries: 3,
});
const teardown = hashbrown.sizzle();
destroyRef.onDestroy(() => teardown());
const messages = toNgSignal(hashbrown.messages);
const internalMessages = computed(() => {
const _input = input();
if (!_input) {
return [];
}
return [
{
role: 'user',
content: _input,
},
];
});
const error = toNgSignal(hashbrown.error, options.debugName && `${options.debugName}.error`);
const exhaustedRetries = toNgSignal(hashbrown.exhaustedRetries, options.debugName && `${options.debugName}.exhaustedRetries`);
effect(() => {
const _messages = internalMessages();
hashbrown.setMessages(_messages);
});
const value = computed(() => {
const lastMessage = messages()[messages().length - 1];
if (lastMessage &&
lastMessage.role === 'assistant' &&
lastMessage.content &&
typeof lastMessage.content === 'string') {
return lastMessage.content;
}
return null;
}, { debugName: options.debugName && `${options.debugName}.value` });
const status = computed(() => {
if (exhaustedRetries()) {
return 'error';
}
return 'idle';
});
const isLoading = signal(false);
const reload = () => {
return true;
};
function hasValue() {
return Boolean(value());
}
function stop(clearStreamingMessage = false) {
hashbrown.stop(clearStreamingMessage);
}
return {
value,
status,
error,
isLoading,
reload,
stop,
hasValue: hasValue,
};
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates a structured chat resource.
*
* @param options - The options for the structured chat resource.
* @returns The structured chat resource.
*/
function structuredChatResource(options) {
const config = ɵinjectHashbrownConfig();
const injector = inject(Injector);
const destroyRef = inject(DestroyRef);
const hashbrown = fryHashbrown({
apiUrl: options.apiUrl ?? config.baseUrl,
middleware: config.middleware?.map((m) => {
return (requestInit) => runInInjectionContext(injector, () => m(requestInit));
}),
system: readSignalLike(options.system),
messages: [...(options.messages ?? [])],
model: readSignalLike(options.model),
tools: options.tools,
responseSchema: options.schema,
debugName: options.debugName,
emulateStructuredOutput: config.emulateStructuredOutput,
debounce: options.debounce,
retries: options.retries,
});
const teardown = hashbrown.sizzle();
destroyRef.onDestroy(() => teardown());
const value = toNgSignal(hashbrown.messages, options.debugName && `${options.debugName}.value`);
const isReceiving = toNgSignal(hashbrown.isReceiving, options.debugName && `${options.debugName}.isReceiving`);
const isSending = toNgSignal(hashbrown.isSending, options.debugName && `${options.debugName}.isSending`);
const isRunningToolCalls = toNgSignal(hashbrown.isRunningToolCalls, options.debugName && `${options.debugName}.isRunningToolCalls`);
const error = toNgSignal(hashbrown.error, options.debugName && `${options.debugName}.error`);
const lastAssistantMessage = toNgSignal(hashbrown.lastAssistantMessage, options.debugName && `${options.debugName}.lastAssistantMessage`);
const exhaustedRetries = toNgSignal(hashbrown.exhaustedRetries);
const status = computed(() => {
if (isReceiving() || isSending() || isRunningToolCalls()) {
return 'loading';
}
if (exhaustedRetries()) {
return 'error';
}
const hasAssistantMessage = value().some((message) => message.role === 'assistant');
if (hasAssistantMessage) {
return 'resolved';
}
return 'idle';
}, { debugName: options.debugName && `${options.debugName}.status` });
const isLoading = computed(() => {
return isReceiving() || isSending() || isRunningToolCalls();
}, { debugName: options.debugName && `${options.debugName}.isLoading` });
function reload() {
const lastMessage = value()[value().length - 1];
if (lastMessage.role === 'assistant') {
hashbrown.setMessages(value().slice(0, -1));
return true;
}
return false;
}
function hasValue() {
return value().some((message) => message.role === 'assistant');
}
function sendMessage(message) {
hashbrown.sendMessage(message);
}
function resendMessages() {
hashbrown.resendMessages();
}
function setMessages(messages) {
hashbrown.setMessages(messages);
}
function stop(clearStreamingMessage = false) {
hashbrown.stop(clearStreamingMessage);
}
return {
hasValue: hasValue,
status,
isLoading,
reload,
sendMessage,
resendMessages,
stop,
value,
error,
setMessages,
lastAssistantMessage,
};
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates a structured completion resource.
*
* @param options - The options for the structured completion resource.
* @returns The structured completion resource.
*/
function structuredCompletionResource(options) {
const { model, input, schema, system, tools, debugName, apiUrl } = options;
const resource = structuredChatResource({
model,
system,
schema,
tools,
debugName,
retries: 3,
apiUrl,
});
effect(() => {
const _input = input();
if (!_input) {
return;
}
resource.setMessages([
{
role: 'user',
content: _input,
},
]);
});
const value = computed(() => {
const lastMessage = resource.value()[resource.value().length - 1];
if (lastMessage &&
lastMessage.role === 'assistant' &&
lastMessage.content &&
lastMessage.content !== null) {
return lastMessage.content;
}
return null;
}, { debugName: debugName && `${debugName}.value` });
const status = resource.status;
const error = resource.error;
const isLoading = resource.isLoading;
const reload = resource.reload;
const stop = resource.stop;
function hasValue() {
return Boolean(value());
}
return {
value,
status,
error,
isLoading,
reload,
stop,
hasValue: hasValue,
};
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates a UI chat resource.
*
* @param args - The arguments for the UI chat resource.
* @returns The UI chat resource.
*/
function uiChatResource(args) {
const flattenedComponents = computed(() => _components.flattenComponents(args.components));
const internalSchema = s.object('UI', {
ui: s.streaming.array('List of elements', _components.createComponentSchema(args.components)),
});
const chat = structuredChatResource({
model: args.model,
schema: internalSchema,
tools: [...(args.tools ?? [])],
system: args.system,
messages: [...(args.messages ?? [])],
debugName: args.debugName,
debounce: args.debounce,
apiUrl: args.apiUrl,
});
const value = computed(() => {
const messages = chat.value();
return messages.map((message) => {
if (message.role === 'assistant') {
const content = message.content;
if (!content) {
return {
...message,
[TAG_NAME_REGISTRY]: {},
};
}
return {
...message,
[TAG_NAME_REGISTRY]: Array.from(flattenedComponents().values()).reduce((acc, component) => {
acc[component.name] = {
props: component.props ?? {},
component: component.component,
};
return acc;
}, {}) ?? {},
};
}
if (message.role === 'user') {
return message;
}
if (message.role === 'error') {
return message;
}
throw new Error(`Unknown message role`);
});
}, { debugName: args.debugName && `${args.debugName}.value` });
const lastAssistantMessage = computed(() => {
return value().findLast((message) => message.role === 'assistant');
});
return {
...chat,
hasValue: chat.hasValue,
value,
lastAssistantMessage,
};
}
/**
* Generated bundle index. Do not edit.
*/
export { RenderMessageComponent, chatResource, completionResource, createRuntime, createRuntimeFunction, createTool, createToolJavaScript, exposeComponent, provideHashbrown, structuredChatResource, structuredCompletionResource, uiChatResource };
//# sourceMappingURL=hashbrownai-angular.mjs.map