Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
220 lines
5.5 KiB
TypeScript
220 lines
5.5 KiB
TypeScript
import { Atom, atom, Box, structuredClone, Vec } from 'tldraw'
|
|
import { FocusedShape } from '../../../shared/format/FocusedShape'
|
|
import {
|
|
AreaContextItem,
|
|
ContextItem,
|
|
PointContextItem,
|
|
ShapeContextItem,
|
|
ShapesContextItem,
|
|
} from '../../../shared/types/ContextItem'
|
|
import type { TldrawAgent } from '../TldrawAgent'
|
|
import { BaseAgentManager } from './BaseAgentManager'
|
|
|
|
/**
|
|
* Manages context items for an agent.
|
|
* Context items are pieces of information that can be sent to the model
|
|
* to provide additional context for the conversation.
|
|
*/
|
|
export class AgentContextManager extends BaseAgentManager {
|
|
/**
|
|
* An atom containing currently selected context items.
|
|
*
|
|
* To send context items to the model, include them in the `contextItems`
|
|
* field of a request.
|
|
*/
|
|
private $contextItems: Atom<ContextItem[]>
|
|
|
|
/**
|
|
* Creates a new context manager for the given agent.
|
|
* Initializes with an empty context items array.
|
|
*/
|
|
constructor(agent: TldrawAgent) {
|
|
super(agent)
|
|
this.$contextItems = atom('contextItems', [])
|
|
}
|
|
|
|
/**
|
|
* Reset the context manager to its initial state.
|
|
* Clears all context items.
|
|
*/
|
|
reset(): void {
|
|
this.$contextItems.set([])
|
|
}
|
|
|
|
/**
|
|
* Get the current context items.
|
|
* @returns An array of context items.
|
|
*/
|
|
getItems() {
|
|
return this.$contextItems.get()
|
|
}
|
|
|
|
/**
|
|
* Set the context items directly.
|
|
* Primarily used for loading persisted state.
|
|
* @param items - The context items to set.
|
|
*/
|
|
setItems(items: ContextItem[]) {
|
|
this.$contextItems.set(items)
|
|
}
|
|
|
|
/**
|
|
* Add a context item to the agent's context, ensuring that duplicates are
|
|
* not included.
|
|
*
|
|
* @param item The context item to add.
|
|
*/
|
|
add(item: ContextItem) {
|
|
this.$contextItems.update((items) => {
|
|
// Don't add shapes that are already within context
|
|
if (item.type === 'shapes') {
|
|
const newItems = dedupeShapesContextItem(item, items)
|
|
return [...items, ...newItems]
|
|
}
|
|
|
|
// Don't add items that are already in context
|
|
if (this.has(item)) {
|
|
return items
|
|
}
|
|
|
|
return [...items, structuredClone(item)]
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Remove a context item from the agent's context.
|
|
* @param item The context item to remove.
|
|
*/
|
|
remove(item: ContextItem) {
|
|
this.$contextItems.update((items) => items.filter((v) => item !== v))
|
|
}
|
|
|
|
/**
|
|
* Clear all context items.
|
|
*/
|
|
clear() {
|
|
this.$contextItems.set([])
|
|
}
|
|
|
|
/**
|
|
* Check if the agent's context contains a specific context item. This could
|
|
* mean as an individual item, or as part of a group of items.
|
|
*
|
|
* @param item The context item to check for.
|
|
* @returns True if the agent's context contains the item, false otherwise.
|
|
*/
|
|
has(item: ContextItem) {
|
|
const items = this.$contextItems.get()
|
|
if (items.some((v) => areContextItemsEqual(v, item))) {
|
|
return true
|
|
}
|
|
|
|
if (item.type === 'shape') {
|
|
for (const existingItem of items) {
|
|
if (existingItem.type === 'shapes') {
|
|
if (existingItem.shapes.some((shape) => shape.shapeId === item.shape.shapeId)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if two context items are equal.
|
|
*
|
|
* This is a helper function that is used internally by the manager.
|
|
*/
|
|
function areContextItemsEqual(a: ContextItem, b: ContextItem): boolean {
|
|
if (a.type !== b.type) return false
|
|
|
|
switch (a.type) {
|
|
case 'shape': {
|
|
const _b = b as ShapeContextItem
|
|
return a.shape.shapeId === _b.shape.shapeId
|
|
}
|
|
case 'shapes': {
|
|
const _b = b as ShapesContextItem
|
|
if (a.shapes.length !== _b.shapes.length) return false
|
|
return a.shapes.every((shape) => _b.shapes.find((s) => s.shapeId === shape.shapeId))
|
|
}
|
|
case 'area': {
|
|
const _b = b as AreaContextItem
|
|
return Box.Equals(a.bounds, _b.bounds)
|
|
}
|
|
case 'point': {
|
|
const _b = b as PointContextItem
|
|
return Vec.Equals(a.point, _b.point)
|
|
}
|
|
default: {
|
|
exhaustiveSwitchError(a)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove duplicate shapes from a shapes context item.
|
|
* If there's only one shape left, return it as a shape item instead.
|
|
*
|
|
* This is a helper function that is used internally by the manager.
|
|
*/
|
|
function dedupeShapesContextItem(
|
|
item: ShapesContextItem,
|
|
existingItems: ContextItem[]
|
|
): ContextItem[] {
|
|
// Get all shape IDs that are already in the context
|
|
const existingShapeIds = new Set<string>()
|
|
|
|
// Check individual shapes
|
|
existingItems.forEach((contextItem) => {
|
|
if (contextItem.type === 'shape') {
|
|
existingShapeIds.add(contextItem.shape.shapeId)
|
|
} else if (contextItem.type === 'shapes') {
|
|
contextItem.shapes.forEach((shape: FocusedShape) => {
|
|
existingShapeIds.add(shape.shapeId)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Filter out shapes that are already in the context
|
|
const newShapes = item.shapes.filter((shape) => !existingShapeIds.has(shape.shapeId))
|
|
|
|
// Only add if there are remaining shapes
|
|
if (newShapes.length > 0) {
|
|
// If only one shape remains, add it as a single shape item
|
|
if (newShapes.length === 1) {
|
|
const newItem: ContextItem = {
|
|
type: 'shape',
|
|
shape: newShapes[0],
|
|
source: item.source,
|
|
}
|
|
return [structuredClone(newItem)]
|
|
}
|
|
|
|
// Otherwise add as a shapes group
|
|
const newItem: ContextItem = {
|
|
type: 'shapes',
|
|
shapes: newShapes,
|
|
source: item.source,
|
|
}
|
|
return [structuredClone(newItem)]
|
|
}
|
|
|
|
// No new shapes to add
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Throw an error if a switch case is not exhaustive.
|
|
*
|
|
* This is a helper function that is used internally by the manager.
|
|
*/
|
|
function exhaustiveSwitchError(value: never, property?: string): never {
|
|
const debugValue =
|
|
property && value && typeof value === 'object' && property in value ? value[property] : value
|
|
throw new Error(`Unknown switch case ${debugValue}`)
|
|
}
|