Skip to content

Commit

Permalink
feat(functions): Wire in schema (#6838)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZaymonFC committed May 25, 2024
1 parent 4922ff9 commit ac34611
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const Story = () => {

export default {
title: 'plugin-chain/PromptTemplate',
component: PromptTemplate,
render: Story,
decorators: [withTheme],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import React, { type PropsWithChildren, useEffect } from 'react';

import { type ChainInput, ChainInputType, type ChainPromptType } from '@braneframe/types';
import { type S } from '@dxos/echo-schema';
import { createDocAccessor } from '@dxos/react-client/echo';
import { DensityProvider, Input, Select, useThemeContext, useTranslation } from '@dxos/react-ui';
import {
Expand Down Expand Up @@ -98,7 +99,7 @@ const usePromptInputs = (prompt: ChainPromptType) => {
}, [prompt.template]);
};

type PromptTemplateProps = { prompt: ChainPromptType; commandEditable?: boolean };
type PromptTemplateProps = { prompt: ChainPromptType; commandEditable?: boolean; schema?: S.Schema<any, any, any> };

export const PromptTemplate = ({ prompt, commandEditable = true }: PromptTemplateProps) => {
const { t } = useTranslation(CHAIN_PLUGIN);
Expand Down Expand Up @@ -135,7 +136,7 @@ export const PromptTemplate = ({ prompt, commandEditable = true }: PromptTemplat
<DensityProvider density='fine'>
<div className={mx('flex flex-col w-full overflow-hidden gap-4', groupBorder)}>
{commandEditable && (
<Section title='Prompt'>
<Section title='Command'>
<div className='flex items-center pl-4'>
<span className='text-neutral-500'>/</span>
<Input.Root>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// Copyright 2024 DXOS.org
//

import React, { type ChangeEventHandler, type FC, type PropsWithChildren, useEffect, useMemo, useState } from 'react';
import React, { type ChangeEventHandler, type FC, type PropsWithChildren, useEffect, useMemo } from 'react';

import { ChainPresets, chainPresets, PromptTemplate } from '@braneframe/plugin-chain';
import { type ChainPromptType } from '@braneframe/types';
import { type DynamicEchoSchema } from '@dxos/echo-schema';
import { DocumentType, FileType, MessageType, SketchType, StackType, type ChainPromptType } from '@braneframe/types';
import { create } from '@dxos/echo-schema';
import {
FunctionDef,
type FunctionTrigger,
Expand All @@ -19,6 +19,16 @@ import {
} from '@dxos/functions/types';
import { Filter, type Space, useQuery } from '@dxos/react-client/echo';
import { DensityProvider, Input, Select } from '@dxos/react-ui';
import { distinctBy } from '@dxos/util';

type TriggerId = string;

const stateInitialValues = {
schemas: [StackType, SketchType, DocumentType, FileType, MessageType] as any[],
selectedSchema: {} as Record<TriggerId, any>,
};

const state = create<typeof stateInitialValues>(stateInitialValues);

const triggerTypes: FunctionTriggerType[] = ['subscription', 'timer', 'webhook', 'websocket'];

Expand All @@ -29,15 +39,38 @@ export const TriggerEditor = ({ space, trigger }: { space: Space; trigger: Funct
[trigger.function, functions],
);

// Initialize meta.
useEffect(() => {
void space.db.schemaRegistry
.getAll()
.then((schemas) => {
// TODO(Zan): We should solve double adding of stored schemas in the schema registry.
state.schemas = distinctBy([...state.schemas, ...schemas], (s) => s.typename).sort((a, b) =>
a.typename < b.typename ? -1 : 1,
);
})
.catch();
}, [space]);

// Keen an enriched version of the schema in memory so we can share it with prompt editor.
useEffect(() => {
const spec = trigger.spec;
if (spec.type === 'subscription') {
if (spec.filter && spec.filter.length > 0) {
const type = spec.filter[0].type;
const foundSchema = state.schemas.find((schema) => schema.typename === type);
if (foundSchema) {
state.selectedSchema[trigger.id] = foundSchema;
}
}
}
}, [JSON.stringify(trigger.spec), state.schemas]);

useEffect(() => {
if (!trigger.meta) {
const extension = metaExtensions[trigger.function];
if (extension && extension.initialValue) {
trigger.meta = extension.initialValue();
}
extension?.initialValue && (trigger.meta = extension.initialValue());
}
}, [trigger.function]);
}, [trigger.function, trigger.meta]);

const handleSelectFunction = (value: string) => {
const match = functions.find((fn) => fn.uri === value);
Expand Down Expand Up @@ -141,26 +174,22 @@ export const TriggerEditor = ({ space, trigger }: { space: Space; trigger: Funct
// Trigger specs
//

const TriggerSpecSubscription = ({ space, spec }: TriggerSpecProps<SubscriptionTrigger>) => {
const [schemas, setSchemas] = useState<DynamicEchoSchema[]>([]);
useEffect(() => {
if (space) {
void space.db.schemaRegistry.getAll().then(setSchemas).catch();
}
}, [space]);
const TriggerSpecSubscription = ({ spec }: TriggerSpecProps<SubscriptionTrigger>) => {
const handleSelectSchema = (typename: string) => {
spec.filter = [{ type: typename }];
};

// TODO(burdon): Wire up (single) filter.
return (
<>
<InputRow label='Filter'>
<Select.Root>
<Select.Root value={spec.filter[0].type} onValueChange={handleSelectSchema}>
<Select.TriggerButton placeholder={'Select type'} />
<Select.Portal>
<Select.Content>
<Select.Viewport>
{schemas.map(({ id }) => (
<Select.Option key={id} value={id}>
{id}
{state.schemas.map(({ typename }: any) => (
<Select.Option key={typename} value={typename}>
{typename}
</Select.Option>
))}
</Select.Viewport>
Expand Down Expand Up @@ -253,20 +282,17 @@ const TriggerSpec = ({ space, spec }: TriggerSpecProps) => {
//

const TriggerMeta = ({ trigger }: { trigger: Partial<FunctionTrigger> }) => {
if (!trigger || !trigger.function || !trigger.meta) {
return null;
}
const meta = useMemo(() => (trigger?.function ? metaExtensions[trigger.function] : undefined), [trigger.function]);

// TODO(burdon): Isn't triggered when function changes.
const { component: Component } = metaExtensions[trigger.function] ?? {};
if (Component) {
const Component = meta?.component;
if (Component && trigger.meta) {
return <Component meta={trigger.meta as any} />;
}

return null;
};

type MetaProps<T> = { meta: T };
type MetaProps<T> = { meta: T } & { triggerId?: string };
type MetaExtension<T> = {
initialValue?: () => T;
component: FC<MetaProps<T>>;
Expand All @@ -286,15 +312,17 @@ const EmailWorkerMeta = ({ meta }: MetaProps<{ account?: string }>) => {
);
};

const ChainPromptMeta = ({ meta }: MetaProps<{ prompt?: ChainPromptType }>) => {
const ChainPromptMeta = ({ meta, triggerId }: MetaProps<{ prompt?: ChainPromptType }>) => {
const schema = triggerId ? state.selectedSchema[triggerId] : undefined;

return (
<>
<InputRow label='Presets'>
<ChainPresets presets={chainPresets} onSelect={(preset) => (meta.prompt = preset.prompt())} />
</InputRow>
{meta.prompt && (
<InputRow label='Prompt'>
<PromptTemplate prompt={meta.prompt} />
<PromptTemplate prompt={meta.prompt} schema={schema} />
</InputRow>
)}
</>
Expand All @@ -308,6 +336,7 @@ const metaExtensions: Record<string, MetaExtension<any>> = {
},

'dxos.org/function/gpt': {
initialValue: () => ({ prompt: undefined }),
component: ChainPromptMeta,
},
};
Expand Down
23 changes: 23 additions & 0 deletions packages/common/util/src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,26 @@ export const diff = <A, B = A>(
// TODO(burdon): Factor out.
export const intersection = <A, B = A>(a: A[], b: B[], comparator: Comparator<A, B>): A[] =>
a.filter((a) => b.find((b) => comparator(a, b)) !== undefined);

/**
* Returns a new array with only the first instance of each unique item
* based on a specified property.
*
* @typeParam T - The type of items in the input array.
* @param array - The array to filter for distinct items.
* @param key - The property key to determine uniqueness for each item.
* @returns A new array with only distinct items based on the specified property.
*/
export const distinctBy = <T, K>(array: T[], selector: (item: T) => K): T[] => {
const seenKeys = new Set<K>();
return array.filter((item) => {
const key = selector(item);

if (seenKeys.has(key)) {
return false;
}

seenKeys.add(key);
return true;
});
};

0 comments on commit ac34611

Please sign in to comment.