Created
August 6, 2025 16:49
-
-
Save Nipsuli/f685d926c89fbcdbeb3b9622f25a36af to your computer and use it in GitHub Desktop.
Exploring ideas on what kind of interface would I like to have on AI memory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Exploring Idea how I'd like to interact with shared memory within my application. | |
| * Like I want to store clear semantic Activities that user does, Users interactions | |
| * with AI Chat and files and other content that are generated by Users. | |
| * | |
| * In a away combination of document RAG, LLM memory, and User beahior derived | |
| * recommendations all in one. | |
| */ | |
| /** | |
| * lazy util for documentation purposes | |
| */ | |
| type ISO8601datetime = string; | |
| /** | |
| * EntityKind describes some type of entity in the application such as: | |
| * - user | |
| * - workspace | |
| * - chat | |
| * - note | |
| * - etc... | |
| */ | |
| export type EntityKind = string; | |
| /** | |
| * When we refer to different kinds of entities we always prefix the id with the | |
| * EntityKind. This makes it easy to infer from any context which kind of entity | |
| * is being referred | |
| */ | |
| export type EntityIdField<T extends EntityKind> = `${T}_id`; | |
| /** | |
| * All entity ids are strings for simplicity. | |
| */ | |
| export type EntityId = string; | |
| /** | |
| * In textual context we can refer entities by just inlining the the id with a | |
| * prefix. Like we could add memory in format of | |
| * | |
| * "user_id:123 is a member in organization_id:321" | |
| */ | |
| export type InlineEntity<T extends EntityKind> = | |
| `${EntityIdField<T>}:${EntityId}`; | |
| /** | |
| * Multiple differnt ways to refernce entity for completenes depending on the | |
| * context. | |
| */ | |
| export type RichEntityId<T extends EntityKind> = | |
| | InlineEntity<T> | |
| | { [K in EntityIdField<T>]: EntityId } | |
| | { | |
| kind: T; | |
| id: EntityId; | |
| }; | |
| /** | |
| * Property might contain information since when the field value has been the way | |
| * it is. Internally each property will have since datetime, and previous values | |
| * are maintained for history. | |
| */ | |
| export type PropertyWithSince = { value: string; since: ISO8601datetime }; | |
| export type Property = string | PropertyWithSince; | |
| /** | |
| * EntityProperties describe some direct factual things about the entity. Such | |
| * as created_at or name or such. The map on purpose is from string to string. | |
| * The key of the property should be descriptive enough, and if there is some | |
| * type on the value the property key should describe it. Like using ISO8601 | |
| * datetime as created_at is quite clear for humans and LLMs. And if one needs | |
| * to use like time stamp then the key probably should be like created_at_ms. | |
| * | |
| * If one needs some form of nested structure one can separate fields with . | |
| * like: | |
| * { | |
| * "profile.image": ..., | |
| * "profile.display_name": ..., | |
| * } | |
| */ | |
| export type Properties = Record<string, Property>; | |
| export type PropertiesWithSince = Record<string, PropertyWithSince>; | |
| export type EntityBase<T extends EntityKind> = { | |
| /** | |
| * The id of entity need to always have the entity kind prefix to make it | |
| * easy to identify entities without having separate discriminator field. | |
| */ | |
| [K in EntityIdField<T>]: EntityId; | |
| }; | |
| export type TextContent = { | |
| type: "text"; | |
| text: string; | |
| }; | |
| export type FileContent = { | |
| type: "file"; | |
| /** | |
| * IANA media type of the file. | |
| * | |
| * https://www.iana.org/assignments/media-types/media-types.xhtml | |
| */ | |
| mediaType: string; | |
| /** | |
| * Optional filename of the file. | |
| */ | |
| filename?: string; | |
| /** | |
| * The URL of the file. | |
| * It can either be a URL to a hosted file or a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs). | |
| */ | |
| url: string; | |
| }; | |
| export type Content = TextContent | FileContent; | |
| /** | |
| * Entity is the core bit of data that gets passed around. In the simplest form | |
| * it's just the id, but it may be as rich with data as needed. | |
| */ | |
| export type Entity< | |
| T extends EntityKind, | |
| P extends Properties = Properties, | |
| > = EntityBase<T> & { | |
| /** | |
| * Entity has optional properties, these can internally be used to fetch Entity | |
| * by known field or filtering. Like entity has some time related fields such | |
| * as created_at or updated_at those can be used for filtering recent data. | |
| * | |
| * In addition of just filtering these fields may be used to build as part of | |
| * of the Entity embedding | |
| */ | |
| properties?: P; | |
| /** | |
| * Entity may contain some attached content. Example in case of Entity is like | |
| * document it might contain text and files and such. This content should be | |
| * analyzed and both vector and graph based representations should be build | |
| * to be able to retrieve content. | |
| */ | |
| content?: Content[]; | |
| /** | |
| * Entity may have relations. Relations are not a separate object in the API | |
| * but nested in the entity it self. | |
| */ | |
| relations?: EntityRelation<EntityKind, Properties>[]; | |
| }; | |
| type EntityRelation<T extends EntityKind, P extends Properties> = { | |
| /** | |
| * The Entity that this is related to. In case the role | |
| */ | |
| entity: Entity<T>; | |
| /** | |
| * The relation can also have properties to map some strict factual data | |
| */ | |
| properties?: P; | |
| /** | |
| * Human (and LLM) readable explanation of the role of the relation | |
| */ | |
| role: string; | |
| }; | |
| /** | |
| * Memory is just predefined Entity with special treatment internally. When | |
| * memory is created and it relates to some other Entities it's also attached | |
| * to the relations of the other Entities | |
| */ | |
| export type Memory = Entity< | |
| "__memory", | |
| Properties & { | |
| status: { | |
| value: "processing" | "ready" | "forgotten"; | |
| since: ISO8601datetime; | |
| }; | |
| created_at: ISO8601datetime; | |
| } | |
| >; | |
| type MemoryOperationEntity<T extends EntityKind> = RichEntityId<T> | Entity<T>; | |
| /** | |
| * The core function for remembering things. In case there are upadates to the | |
| * entities the entities are updated, and more recent info will override the | |
| * data. | |
| */ | |
| export type RememberFn = { | |
| /** | |
| * Basic string based memory, things are parsed directly from the | |
| * memory string, e.g. it might contain InlineEntity things | |
| */ | |
| (memory: string): Promise<Memory>; | |
| /** | |
| * textual memory with attached entities | |
| */ | |
| ( | |
| memory: string, | |
| entities: MemoryOperationEntity<EntityKind>[], | |
| ): Promise<Memory>; | |
| /** | |
| * Just bulk saving some entities, either creating or updating entities | |
| */ | |
| (entities: MemoryOperationEntity<EntityKind>[]): Promise<Memory>; | |
| /** | |
| * Template literal function for utility | |
| */ | |
| ( | |
| memory: TemplateStringsArray, | |
| ...entities: MemoryOperationEntity<EntityKind>[] | |
| ): Promise<Memory>; | |
| }; | |
| /** | |
| * The core function for forgetting things. | |
| */ | |
| export type ForgetFn = { | |
| /** | |
| * Forget this fact. Text can contian InlineEntitys | |
| */ | |
| (memory: string): Promise<Memory>; | |
| /** | |
| * textual forgetting with attached entities | |
| */ | |
| ( | |
| memory: string, | |
| entities: MemoryOperationEntity<EntityKind>[], | |
| ): Promise<Memory>; | |
| /** | |
| * bulk forgetting some entities | |
| */ | |
| (entities: MemoryOperationEntity<EntityKind>[]): Promise<Memory>; | |
| /** | |
| * Bulk forgetting some entities. If partial is true only those fields that the | |
| * passed in entity has are forgotten. Like can be relation or property field. | |
| */ | |
| ( | |
| entities: MemoryOperationEntity<EntityKind>[], | |
| partial: boolean, | |
| ): Promise<Memory>; | |
| /** | |
| * Template literal function for utility | |
| */ | |
| ( | |
| memory: TemplateStringsArray, | |
| ...entities: MemoryOperationEntity<EntityKind>[] | |
| ): Promise<Memory>; | |
| /** | |
| * Explicit forgetting of a memory | |
| */ | |
| (memory: Memory): Promise<Memory>; | |
| }; | |
| export type RecallEntity< | |
| T extends EntityKind, | |
| Props extends boolean = false, | |
| Content extends boolean = false, | |
| Relations extends boolean = false, | |
| > = EntityBase<T> & Props extends true | |
| ? { properties: PropertiesWithSince } | |
| : {} & Content extends true | |
| ? { content: Content } | |
| : {} & Relations extends true | |
| ? { | |
| relations: { | |
| entity: EntityBase<EntityKind> & Props extends true | |
| ? { properties: PropertiesWithSince } | |
| : {}; | |
| role: string; | |
| } & Props extends true | |
| ? { properties: PropertiesWithSince } | |
| : {}; | |
| } | |
| : {}; | |
| export type EntityGetFn = { | |
| <T extends EntityKind>( | |
| id: RichEntityId<T>, | |
| ): Promise<RecallEntity<T, false, false, false> | null>; | |
| < | |
| T extends EntityKind, | |
| Props extends boolean, | |
| Content extends boolean, | |
| Relations extends boolean, | |
| >( | |
| id: RichEntityId<T>, | |
| opts: { | |
| props: Props; | |
| content: Content; | |
| relations: Relations; | |
| }, | |
| ): Promise<RecallEntity<T, Props, Content, Relations> | null>; | |
| }; | |
| export type RecallResult = { | |
| /** | |
| * Text answer to the recall call | |
| */ | |
| text: string; | |
| /** | |
| * Minimal references to the entities that contributed to the answer | |
| * Returned as InlineEntities[] to avoid returning objects. Caller can then | |
| * decide to do what ever they want with the information. Like fetch more | |
| * details about them for display purposes | |
| */ | |
| grounding: InlineEntity<EntityKind>[]; | |
| }; | |
| export type RecallFn = { | |
| /** | |
| * Basic non context generic recall | |
| */ | |
| (query: string): Promise<RecallResult>; | |
| /** | |
| * recall with some question or prompt in given context | |
| */ | |
| ( | |
| query: string, | |
| context: MemoryOperationEntity<EntityKind>[], | |
| ): Promise<RecallResult>; | |
| /** | |
| * recall within some context. Example can be used to call when user opens a | |
| * document to get some memory what was going on last time they were there | |
| */ | |
| (query: MemoryOperationEntity<EntityKind>[]): Promise<RecallResult>; | |
| /** | |
| * Template literal function for utility | |
| */ | |
| ( | |
| query: TemplateStringsArray, | |
| ...context: MemoryOperationEntity<EntityKind>[] | |
| ): Promise<RecallResult>; | |
| }; | |
| /** | |
| * The super mega powerful memory client. | |
| */ | |
| export type MemoryClient = { | |
| remember: RememberFn; | |
| forget: ForgetFn; | |
| recall: RecallFn; | |
| /** | |
| * Utility for pulling the exact entity out | |
| */ | |
| getEntity: EntityGetFn; | |
| }; | |
| export type CreateMemoryClientFn = (opts: { | |
| apiKey: string; | |
| /** | |
| * Each namespace is isolated and within namespace one has Access to everything. | |
| * There should not be any need to manually create or manage namespaces. Similarly | |
| * as there should not be any need to manage which entities can and cannot be there. | |
| * just calling with new namespace or entity kind just does needed things. | |
| */ | |
| namespace: string; | |
| }) => MemoryClient; | |
| // | |
| // USAGE | |
| // | |
| declare const memoryClient: MemoryClient; | |
| // Create some memories | |
| // just text memory with InlineEntityId | |
| const memory = await memoryClient.remember("user_id:123 likes bananas"); | |
| // product event type of memory | |
| await memoryClient.remember("User joined orgazation", [ | |
| { | |
| user_id: "123", | |
| properties: { username: "Nipsuli", nickname: "The Data Cowboy" }, | |
| }, | |
| // alternative way to flag contextual entity | |
| { kind: "organization", id: "321" }, | |
| ]); | |
| // remember some object | |
| await memoryClient.remember([ | |
| { | |
| note_id: "abc", | |
| content: [ | |
| { | |
| type: "text", | |
| text: "Some note...", | |
| }, | |
| ], | |
| relations: [ | |
| { | |
| entity: { user_id: "123" }, | |
| role: "Created by", | |
| }, | |
| ], | |
| }, | |
| ]); | |
| // with tag literal | |
| const user = { user_id: "123" }; | |
| const chatMessage = { | |
| chat_message_id: "def", | |
| properties: { | |
| chat_message_role: "user", | |
| }, | |
| relations: [ | |
| { | |
| chat_id: "qwe", | |
| role: "Belongs to", | |
| }, | |
| ], | |
| }; | |
| await memoryClient.remember`${user} sent ${chatMessage}`; | |
| // Forgetting things | |
| // explicit forgetting | |
| await memoryClient.forget(memory); | |
| // fuzzy forgetting | |
| await memoryClient.forget("Evertying that happened in Vegas"); | |
| // forgetting in context | |
| await memoryClient.forget("All the things user has done", [{ user_id: "123" }]); | |
| // forgetting these entities ever existed | |
| await memoryClient.forget([ | |
| // InlineEntity works | |
| "note_id:abc", | |
| // as with other formats for entity | |
| { file_id: "xyz" }, | |
| ]); | |
| // partial forgetting | |
| await memoryClient.forget( | |
| [ | |
| { | |
| user_id: "123", | |
| properties: { | |
| // the value doesn't matter if we're forgetting property | |
| nickname: "", | |
| }, | |
| }, | |
| ], | |
| // partial forgetting: will only forget properties and relations that | |
| // are passed in | |
| true, | |
| ); | |
| // template literal | |
| const user2 = "user_id:123"; | |
| const movie = { | |
| movie_id: "Human centipede", | |
| }; | |
| await memoryClient.forget`${user2} saw ${movie}`; | |
| // Recalling | |
| // Generic recall | |
| await memoryClient.recall( | |
| "What was the pricing that we originally thought for this service?", | |
| ); | |
| // Contextual recall with query | |
| await memoryClient.recall("What have I done during this week?", [ | |
| { user_id: "123" }, | |
| ]); | |
| // Generic contextual recall | |
| await memoryClient.recall([ | |
| // again both InlineEntity | |
| "note_id:abc", | |
| // And other Entity formats work | |
| { user_id: "123" }, | |
| ]); | |
| // And templates | |
| const recallRes = | |
| await memoryClient.recall`What did ${user2} think about ${movie}?`; | |
| console.log(recallRes.text); // Can't remember Nipsuli ever seeing movie Human centipede |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment