Compare commits

...
Author SHA1 Message Date
Nicolò Boschi eb2572331c doc: improvements 2026-02-27 14:51:43 +01:00
76 changed files with 684 additions and 7596 deletions
@@ -4,6 +4,7 @@ description: Learn how Hindsight handles contradictory information by tracking t
authors: [hindsight]
image: /img/blog/2026-02-09/consolidation-pipeline.png
date: 2026-02-09
hide_table_of_contents: true
---
# How We Solved Memory Conflicts in Hindsight
@@ -3,6 +3,7 @@ title: "What's new in Hindsight 0.4.11"
description: New features and improvements in Hindsight 0.4.11
authors: [hindsight]
date: 2026-02-13
hide_table_of_contents: true
---
Hindsight 0.4.11 focuses on production-ready deployments with improved flexibility and observability.
@@ -3,6 +3,7 @@ title: "What's new in Hindsight 0.4.12"
description: New features and improvements in Hindsight 0.4.12
authors: [hindsight]
date: 2026-02-18
hide_table_of_contents: true
---
Hindsight 0.4.12 expands what you can ingest, cuts ingestion costs, and broadens where you can run it.
+4 -2
View File
@@ -1,11 +1,13 @@
---
title: "I Gave My Vercel Chat SDK Bot a Memory. Now It Remembers Users Across Slack and Discord."
title: "Your Vercel Chat SDK bot forgets everything. Hindsight fixes that."
authors: [hindsight]
date: 2026-02-26
tags: [chat-sdk, slack, discord, typescript, memory]
image: /img/blog/vercel-chat.png
---
# I Gave My Vercel Chat SDK Bot a Memory. Now It Remembers Users Across Slack and Discord.
![Consolidation Pipeline](/img/blog/vercel-chat.png)
## TL;DR
@@ -4,7 +4,7 @@ sidebar_position: 5
# Vercel Chat SDK
The `@vectorize-io/hindsight-chat` package gives your [Vercel Chat SDK](https://github.com/vercel/chat) bots persistent, per-user memory with a single handler wrapper. Works with Slack, Discord, Teams, Google Chat, GitHub, and Linear.
We built `@vectorize-io/hindsight-chat` to give [Vercel Chat SDK](https://github.com/vercel/chat) bots persistent, per-user memory with a single handler wrapper. The integration works across Slack, Discord, Teams, Google Chat, GitHub, and Linear — no custom plumbing required.
## Installation
@@ -60,7 +60,7 @@ chat.onNewMention(
### `withHindsightChat(options, handler)`
Returns a standard Chat SDK handler `(thread, message) => Promise<void>`.
`withHindsightChat` wraps your existing Chat SDK handler and injects memory context automatically. It returns a standard handler `(thread, message) => Promise<void>` so it drops in without changing your handler signature.
#### Options
@@ -80,7 +80,7 @@ Returns a standard Chat SDK handler `(thread, message) => Promise<void>`.
### Context (`ctx`)
The third argument passed to your handler:
We inject a third `ctx` argument into your handler that exposes the full Hindsight memory API scoped to the current user's bank:
| Property/Method | Description |
|----------------|-------------|
@@ -160,4 +160,4 @@ chat.onNewMention(
## Error Handling
Memory failures never break your bot. Auto-recall and auto-retain errors are logged as warnings and the handler continues with empty memories. Manual `ctx.retain()`, `ctx.recall()`, and `ctx.reflect()` calls propagate errors normally so you can handle them as needed.
We designed the integration so that memory failures never break your bot. Auto-recall and auto-retain errors are caught internally, logged as warnings, and the handler continues with empty memories. Manual `ctx.retain()`, `ctx.recall()`, and `ctx.reflect()` calls propagate errors normally so you can handle them as needed.
+8 -10
View File
@@ -117,8 +117,7 @@ const config: Config = {
blogTitle: 'Hindsight Blog',
blogDescription: 'Updates, insights, and deep dives into agent memory',
postsPerPage: 10,
blogSidebarTitle: 'Recent posts',
blogSidebarCount: 'ALL',
blogSidebarCount: 0,
},
theme: {
customCss: './src/css/custom.css',
@@ -235,8 +234,7 @@ const config: Config = {
className: 'navbar-item-resources',
items: [
{
type: 'doc',
docId: 'cookbook/index',
to: '/cookbook',
label: 'Cookbook',
},
{
@@ -247,22 +245,22 @@ const config: Config = {
to: '/api-reference',
label: 'API Reference',
},
{
href: 'https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg',
label: 'Community',
},
],
},
{
href: 'https://ui.hindsight.vectorize.io/signup',
position: 'right',
label: 'Hindsight Cloud',
label: 'Cloud',
className: 'navbar-item-cloud',
},
{
type: 'docsVersionDropdown',
position: 'right',
},
{
href: 'https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg',
position: 'right',
label: 'Community',
className: 'navbar-item-version',
},
{
href: 'https://github.com/vectorize-io/hindsight',
-7
View File
@@ -234,13 +234,6 @@ const sidebars: SidebarsConfig = {
],
},
],
cookbookSidebar: [
{
type: 'doc',
id: 'cookbook/index',
label: 'Cookbook',
},
],
};
export default sidebars;
@@ -0,0 +1,115 @@
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
margin-bottom: 3rem;
}
@media (max-width: 996px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr;
}
}
/* Card */
.card {
display: flex;
flex-direction: column;
border-radius: 10px;
border: 1px solid var(--ifm-color-emphasis-200);
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #0074d9, #009296) 1;
background: var(--ifm-background-surface-color);
text-decoration: none !important;
color: inherit;
transition: box-shadow 0.2s ease, transform 0.2s ease;
overflow: hidden;
}
[data-theme='dark'] .card {
background: #1c1c1e;
border-color: rgba(255, 255, 255, 0.07);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 116, 217, 0.12);
text-decoration: none !important;
color: inherit;
}
/* Body */
.cardBody {
display: flex;
flex-direction: column;
flex: 1;
padding: 1.25rem 1.25rem 1.25rem;
gap: 0.4rem;
}
.cardTitle {
font-size: 1rem;
font-weight: 700;
line-height: 1.4;
margin: 0;
color: var(--ifm-heading-color);
letter-spacing: -0.01em;
border-bottom: none !important;
border-image: none !important;
}
.cardDescription {
font-size: 0.82rem;
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
color: var(--ifm-color-emphasis-700);
line-height: 1.6;
margin: 0;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.cardFooter {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.75rem;
padding-top: 0.65rem;
border-top: 1px solid var(--ifm-color-emphasis-100);
}
[data-theme='dark'] .cardFooter {
border-top-color: rgba(255, 255, 255, 0.06);
}
.cardTopic {
font-size: 0.72rem;
font-weight: 600;
color: var(--ifm-color-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.cardSdk {
font-size: 0.72rem;
font-weight: 500;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--ifm-color-emphasis-700);
background: var(--ifm-color-emphasis-100);
padding: 0.1rem 0.45rem;
border-radius: 4px;
}
[data-theme='dark'] .cardSdk {
background: rgba(255, 255, 255, 0.07);
color: var(--ifm-color-emphasis-600);
}
@@ -0,0 +1,44 @@
import React from 'react';
import Link from '@docusaurus/Link';
import styles from './CookbookGrid.module.css';
export interface CookbookCard {
title: string;
href: string;
description?: string;
tags?: {
sdk?: string;
topic?: string;
};
}
interface CookbookGridProps {
items: CookbookCard[];
}
function Card({title, href, description, tags}: CookbookCard) {
return (
<Link to={href} className={styles.card}>
<div className={styles.cardBody}>
<h3 className={styles.cardTitle}>{title}</h3>
{description && <p className={styles.cardDescription}>{description}</p>}
{(tags?.topic || tags?.sdk) && (
<div className={styles.cardFooter}>
{tags.topic && <span className={styles.cardTopic}>{tags.topic}</span>}
{tags.sdk && <span className={styles.cardSdk}>{tags.sdk}</span>}
</div>
)}
</div>
</Link>
);
}
export default function CookbookGrid({items}: CookbookGridProps) {
return (
<div className={styles.grid}>
{items.map((item) => (
<Card key={item.href} {...item} />
))}
</div>
);
}
@@ -1,181 +0,0 @@
.carouselSection {
margin: 3rem 0;
}
.sectionTitle {
font-size: 1.75rem;
margin-bottom: 1.5rem;
font-weight: 600;
color: var(--ifm-font-color-base);
}
.carousel {
/* Grid layout */
}
.carouselTrack {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.25rem;
}
.card {
padding: 1.5rem;
border-radius: 12px;
border: 2px solid var(--card-border, var(--ifm-color-emphasis-300));
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
min-height: 200px;
}
/* Alternating style: Odd cards = white/solid, Even cards = colored gradient */
/* ODD CARDS - White/Solid background */
.card:nth-child(odd) {
background: #ffffff;
}
.card:nth-child(odd):hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
/* EVEN CARDS - Colored gradients (cycle through 4 colors) */
.card:nth-child(4n+2) {
background: linear-gradient(135deg, rgba(0, 116, 217, 0.08) 0%, rgba(0, 146, 150, 0.08) 100%);
}
.card:nth-child(4n+4) {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(168, 85, 247, 0.08) 100%);
}
.card:nth-child(4n+6) {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(5, 150, 105, 0.08) 100%);
}
.card:nth-child(4n+8) {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(217, 119, 6, 0.08) 100%);
}
.card:nth-child(even):hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
/* ============================================
DARK MODE
============================================ */
/* ODD CARDS - Dark solid background */
[data-theme='dark'] .card:nth-child(odd) {
background: var(--ifm-background-surface-color);
}
[data-theme='dark'] .card {
border-color: var(--card-border-dark, var(--ifm-color-emphasis-300));
}
[data-theme='dark'] .card:nth-child(odd):hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
/* EVEN CARDS - Colored gradients (more vibrant in dark mode) */
[data-theme='dark'] .card:nth-child(4n+2) {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(20, 184, 166, 0.15) 100%);
}
[data-theme='dark'] .card:nth-child(4n+4) {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(217, 70, 239, 0.15) 100%);
}
[data-theme='dark'] .card:nth-child(4n+6) {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(132, 204, 22, 0.15) 100%);
}
[data-theme='dark'] .card:nth-child(4n+8) {
background: linear-gradient(135deg, rgba(251, 146, 60, 0.15) 0%, rgba(239, 68, 68, 0.15) 100%);
}
[data-theme='dark'] .card:nth-child(even):hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.8);
}
.cardContent {
display: flex;
flex-direction: column;
gap: 0.65rem;
flex-grow: 1;
}
.cardFooter {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.cardTitle {
font-size: 1.05rem;
font-weight: 600;
color: var(--ifm-font-color-base);
line-height: 1.4;
}
.cardDescription {
font-size: 0.9rem;
color: var(--ifm-color-emphasis-800);
margin: 0;
line-height: 1.6;
}
[data-theme='dark'] .cardDescription {
color: var(--ifm-color-emphasis-600);
}
.cardTags {
display: flex;
flex-wrap: nowrap;
gap: 0.75rem;
align-items: center;
flex: 1;
}
.tag {
font-size: 0.72rem;
padding: 0.35rem 0.75rem;
border-radius: 6px;
background: var(--tag-bg);
color: var(--tag-text);
font-weight: 600;
border: 1px solid var(--tag-border);
white-space: nowrap;
flex-shrink: 0;
}
[data-theme='dark'] .tag {
background: var(--tag-bg-dark);
color: var(--tag-text-dark);
border-color: var(--tag-border-dark);
}
.cardLink {
color: var(--ifm-color-primary);
font-weight: 600;
flex-shrink: 0;
font-size: 1.25rem;
line-height: 1;
opacity: 0.7;
transition: opacity 0.2s ease;
margin-left: 1rem;
}
.card:hover .cardLink {
opacity: 1;
}
@@ -1,171 +0,0 @@
import React from 'react';
import Link from '@docusaurus/Link';
import styles from './RecipeCarousel.module.css';
export interface RecipeCard {
title: string;
href: string;
tags?: {
sdk?: string; // Package name: "hindsight-python", "hindsight-nodejs", "litellm-python", "ai-sdk", etc.
topic?: string; // "Learning", "Quick Start", "Recommendation", "Chat"
};
description?: string;
}
interface RecipeCarouselProps {
title: string;
items: RecipeCard[];
}
// Language icons using inline SVG data URIs or image paths
const LANGUAGE_ICONS: Record<string, string> = {
Python: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%233776ab' d='M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z'/%3E%3C/svg%3E",
'Node.js': "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23339933' d='M11.998 0c-.27 0-.54.07-.772.202L2.428 5.05C1.983 5.321 1.7 5.802 1.7 6.32v11.36c0 .518.283 1 .728 1.27l2.375 1.371c.64.321 1.094.32 1.468.32 1.203 0 1.89-.73 1.89-1.996V7.362c0-.146-.117-.264-.262-.264H7.11c-.146 0-.263.118-.263.264v11.283c0 .876-.906 1.753-2.38 1.01L2.103 18.28c-.046-.026-.073-.08-.073-.132V6.754c0-.051.027-.106.073-.132l8.798-5.08c.044-.026.102-.026.145 0l8.798 5.08c.046.026.074.081.074.132v11.394c0 .051-.028.106-.074.132l-8.798 5.08c-.043.026-.101.026-.144 0l-2.248-1.336c-.064-.037-.144-.04-.21-.011-.55.307-.658.373-1.177.45-.12.019-.301.06.073.276l2.93 1.738c.23.133.49.202.772.202s.542-.069.772-.202l8.798-5.08c.476-.27.772-.772.772-1.27V6.32c0-.518-.296-.999-.772-1.27L12.77.202C12.538.07 12.268 0 11.998 0zm2.657 6.343c-2.432 0-2.945.953-2.945 2.146 0 .145.117.263.263.263h.788c.131 0 .24-.095.261-.221.177-.718.708-1.08 1.633-1.08.738 0 1.177.168 1.177.803 0 .325-.128.567-.678.73l-1.69.419c-.899.223-1.47.756-1.47 1.636 0 1.076.905 1.715 2.423 1.715 1.704 0 2.55-.593 2.656-1.866.006-.073-.018-.144-.066-.197-.047-.053-.114-.083-.186-.083h-.791c-.123 0-.23.089-.258.207-.286.644-.98.849-1.817.849-.65 0-1.16-.207-1.16-.725 0-.325.144-.424.903-.609l1.476-.367c.898-.223 1.462-.72 1.462-1.613 0-1.12-.937-1.787-2.574-1.787z'/%3E%3C/svg%3E",
TypeScript: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%233178c6' d='M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111.472.074.914.187 1.323.34v2.458c-.444-.223-.935-.39-1.473-.501-.539-.111-1.09-.167-1.655-.167-.562 0-1.011.062-1.349.187-.338.124-.507.335-.507.632 0 .234.095.42.285.558.19.138.503.275.94.411l1.503.434c.915.262 1.577.609 1.984 1.04.408.432.612.998.612 1.699 0 .915-.35 1.638-1.05 2.168-.7.53-1.667.795-2.9.795-.591 0-1.178-.051-1.76-.153-.582-.102-1.13-.258-1.645-.468v-2.503c.544.287 1.09.507 1.637.66.546.153 1.084.23 1.613.23.609 0 1.071-.073 1.386-.219.315-.146.472-.369.472-.669 0-.262-.106-.471-.318-.628-.212-.157-.551-.306-1.017-.447l-1.42-.395c-.877-.234-1.515-.563-1.916-.985-.4-.422-.6-.98-.6-1.673 0-.857.348-1.545 1.044-2.063.696-.518 1.633-.777 2.811-.777zm-13.6 1.77H8.45l-.031 4.18c0 .754-.13 1.314-.39 1.68-.26.367-.65.55-1.168.55-.286 0-.56-.037-.822-.11-.262-.074-.506-.173-.733-.297v1.818c.319.111.665.187 1.038.228.373.04.736.06 1.089.06.924 0 1.623-.247 2.097-.74.474-.494.711-1.254.711-2.28V11.52z'/%3E%3C/svg%3E",
Go: "/img/icons/golang.png",
};
// Get language icon based on package name
function getPackageIcon(packageName: string): string | undefined {
// If it starts with @vectorize-io, it's Node.js
if (packageName.startsWith('@vectorize-io')) {
return LANGUAGE_ICONS['Node.js'];
}
// If it ends with -go or contains go-, it's Go
if (packageName.endsWith('-go') || packageName.includes('go-')) {
return LANGUAGE_ICONS.Go;
}
// Otherwise assume Python
return LANGUAGE_ICONS.Python;
}
// Generate color scheme from tag text using hash
function getTagColor(tag: string): any {
// Hash function to get consistent color from string
let hash = 0;
for (let i = 0; i < tag.length; i++) {
hash = tag.charCodeAt(i) + ((hash << 5) - hash);
}
// 12 vibrant color palettes with better contrast
const palettes = [
{ h: 340, s: 75, l: 50 }, // Pink
{ h: 291, s: 65, l: 45 }, // Purple
{ h: 262, s: 55, l: 48 }, // Deep Purple
{ h: 231, s: 50, l: 50 }, // Indigo
{ h: 207, s: 80, l: 50 }, // Blue
{ h: 199, s: 85, l: 45 }, // Light Blue
{ h: 187, s: 70, l: 45 }, // Cyan
{ h: 174, s: 70, l: 50 }, // Teal
{ h: 142, s: 65, l: 45 }, // Green
{ h: 88, s: 55, l: 48 }, // Light Green
{ h: 38, s: 85, l: 50 }, // Orange
{ h: 14, s: 85, l: 50 }, // Deep Orange
];
const palette = palettes[Math.abs(hash) % palettes.length];
const { h, s, l } = palette;
return {
// Light mode: subtle background, darker text for contrast
bg: `hsla(${h}, ${s}%, ${l}%, 0.15)`,
text: `hsl(${h}, ${Math.min(s + 10, 90)}%, ${Math.max(l - 25, 25)}%)`,
border: `hsla(${h}, ${s}%, ${l}%, 0.35)`,
// Dark mode: more vibrant background, lighter text
bgDark: `hsla(${h}, ${Math.max(s - 10, 50)}%, ${l}%, 0.25)`,
textDark: `hsl(${h}, ${Math.max(s - 15, 40)}%, ${Math.min(l + 35, 85)}%)`,
borderDark: `hsla(${h}, ${s}%, ${l}%, 0.4)`,
};
}
export default function RecipeCarousel({ title, items }: RecipeCarouselProps): React.ReactElement {
// Generate ID from title for anchor links
const sectionId = title.toLowerCase().replace(/\s+/g, '-');
return (
<div className={styles.carouselSection} id={sectionId}>
<h2 className={styles.sectionTitle}>{title}</h2>
<div className={styles.carousel}>
<div className={styles.carouselTrack}>
{items.map((item, index) => {
// Get topic color for card border
const topicColors = item.tags?.topic ? getTagColor(item.tags.topic) : null;
return (
<Link
key={index}
to={item.href}
className={styles.card}
style={{
'--card-border': topicColors?.border,
'--card-border-dark': topicColors?.borderDark,
} as React.CSSProperties}
>
<div className={styles.cardContent}>
<span className={styles.cardTitle}>{item.title}</span>
{item.description && (
<p className={styles.cardDescription}>{item.description}</p>
)}
</div>
<div className={styles.cardFooter}>
{item.tags && (
<div className={styles.cardTags}>
{item.tags.sdk && (() => {
const colors = getTagColor(item.tags.sdk);
const icon = getPackageIcon(item.tags.sdk);
return (
<span
className={styles.tag}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.4rem',
'--tag-bg': colors.bg,
'--tag-text': colors.text,
'--tag-border': colors.border,
'--tag-bg-dark': colors.bgDark,
'--tag-text-dark': colors.textDark,
'--tag-border-dark': colors.borderDark,
} as React.CSSProperties}
>
{icon && (
<img
src={icon}
alt=""
style={{ width: '13px', height: '13px', flexShrink: 0 }}
/>
)}
{item.tags.sdk}
</span>
);
})()}
{item.tags.topic && (() => {
const colors = getTagColor(item.tags.topic);
return (
<span
className={styles.tag}
style={{
'--tag-bg': colors.bg,
'--tag-text': colors.text,
'--tag-border': colors.border,
'--tag-bg-dark': colors.bgDark,
'--tag-text-dark': colors.textDark,
'--tag-border-dark': colors.borderDark,
} as React.CSSProperties}
>
{item.tags.topic}
</span>
);
})()}
</div>
)}
<span className={styles.cardLink}></span>
</div>
</Link>
);
})}
</div>
</div>
</div>
);
}
@@ -92,6 +92,43 @@
display: block;
}
.titleRow {
display: flex;
align-items: center;
gap: 8px;
}
.copyPageButton {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 3px 7px;
color: rgba(255, 255, 255, 0.85);
font-size: 10px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: all 0.2s ease;
}
.copyPageButton:hover {
background: rgba(255, 255, 255, 0.22);
color: white;
}
.copyPageButton:active {
transform: scale(0.95);
}
.copyPageButton.copyPageCopied {
background: rgba(255, 255, 255, 0.22);
color: white;
}
/* Dark mode - make it stand out even more */
html[data-theme='dark'] .banner {
box-shadow: 0 4px 12px rgba(0, 116, 217, 0.2),
+112 -13
View File
@@ -1,39 +1,138 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import styles from './SkillBanner.module.css';
function extractMarkdown(element: Element): string {
let text = '';
const processNode = (node: Node): string => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || '';
}
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
const tagName = el.tagName.toLowerCase();
const children = Array.from(el.childNodes).map(processNode).join('');
switch (tagName) {
case 'h1': return `# ${children}\n\n`;
case 'h2': return `## ${children}\n\n`;
case 'h3': return `### ${children}\n\n`;
case 'h4': return `#### ${children}\n\n`;
case 'h5': return `##### ${children}\n\n`;
case 'h6': return `###### ${children}\n\n`;
case 'p': return `${children}\n\n`;
case 'ul': return `${children}\n`;
case 'ol': return `${children}\n`;
case 'li': {
const parent = el.parentElement;
const isOrdered = parent?.tagName.toLowerCase() === 'ol';
if (isOrdered) {
const index = Array.from(parent?.children || []).indexOf(el) + 1;
return `${index}. ${children}\n`;
}
return `- ${children}\n`;
}
case 'code': {
const isBlock = el.parentElement?.tagName.toLowerCase() === 'pre';
if (isBlock) {
const lang = el.className.replace('language-', '');
return `\`\`\`${lang}\n${children}\n\`\`\`\n\n`;
}
return `\`${children}\``;
}
case 'pre': return children;
case 'blockquote': return children.split('\n').map(line => `> ${line}`).join('\n') + '\n\n';
case 'a': return `[${children}](${el.getAttribute('href') || ''})`;
case 'strong': case 'b': return `**${children}**`;
case 'em': case 'i': return `*${children}*`;
case 'br': return '\n';
case 'hr': return '---\n\n';
case 'table': return `${children}\n`;
case 'thead': case 'tbody': return children;
case 'tr': return `${children}|\n`;
case 'th': case 'td': return `| ${children} `;
case 'img': return `![${el.getAttribute('alt') || ''}](${el.getAttribute('src') || ''})`;
default: return children;
}
}
return '';
};
Array.from(element.childNodes).forEach(node => { text += processNode(node); });
return text;
}
export default function SkillBanner(): JSX.Element {
const [copied, setCopied] = useState(false);
const [commandCopied, setCommandCopied] = useState(false);
const [pageCopied, setPageCopied] = useState(false);
const command = 'npx skills add https://github.com/vectorize-io/hindsight --skill hindsight-docs';
const handleCopy = async () => {
const handleCopyCommand = async () => {
try {
await navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
setCommandCopied(true);
setTimeout(() => setCommandCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleCopyPage = useCallback(async () => {
try {
const contentElement = document.querySelector('.markdown');
if (!contentElement) return;
const title = document.querySelector('h1')?.textContent;
let markdown = title ? `# ${title}\n\n` : '';
const contentToCopy = Array.from(contentElement.children)
.filter(child => !(child.tagName === 'H1' && child.textContent === title))
.map(child => extractMarkdown(child))
.join('');
markdown += contentToCopy;
markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
await navigator.clipboard.writeText(markdown);
setPageCopied(true);
setTimeout(() => setPageCopied(false), 2000);
} catch (error) {
console.error('Failed to copy page content:', error);
}
}, []);
return (
<div className={styles.container}>
<div className={styles.banner}>
<div className={styles.icon}>🤖</div>
<div className={styles.content}>
<div className={styles.title}>
Using a coding agent? Install the docs skill for instant access
<div className={styles.titleRow}>
<div className={styles.title}>
Using a coding agent? Run this to install the Hindsight docs skill:
</div>
<button
className={`${styles.copyPageButton} ${pageCopied ? styles.copyPageCopied : ''}`}
onClick={handleCopyPage}
aria-label="Export page as markdown"
title={pageCopied ? 'Copied!' : 'Export page as markdown'}
>
{pageCopied ? (
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
) : (
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6z"/>
<path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2z"/>
</svg>
)}
<span>{pageCopied ? 'Copied!' : 'export this page as .md'}</span>
</button>
</div>
<div className={styles.commandWrapper}>
<code className={styles.command}>
{command}
</code>
<code className={styles.command}>{command}</code>
<button
className={styles.copyButton}
onClick={handleCopy}
onClick={handleCopyCommand}
aria-label="Copy command"
title={copied ? 'Copied!' : 'Copy to clipboard'}
title={commandCopied ? 'Copied!' : 'Copy to clipboard'}
>
{copied ? (
{commandCopied ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
+50 -146
View File
@@ -94,65 +94,6 @@
transition: background-color 0.15s ease;
}
/* Navbar icons (desktop only) */
@media (min-width: 1400px) {
.navbar-item-developer::before,
.navbar-item-sdks::before,
.navbar-item-api::before,
.navbar-item-cookbook::before,
.navbar-item-changelog::before {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 6px;
vertical-align: middle;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
content: '';
}
.navbar-item-developer::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M71.68 97.22 34.74 128l36.94 30.78a12 12 0 1 1-15.36 18.44l-48-40a12 12 0 0 1 0-18.44l48-40a12 12 0 0 1 15.36 18.44Zm176 21.56-48-40a12 12 0 1 0-15.36 18.44L221.26 128l-36.94 30.78a12 12 0 1 0 15.36 18.44l48-40a12 12 0 0 0 0-18.44ZM164.1 28.72a12 12 0 0 0-15.38 7.18l-64 176a12 12 0 0 0 7.18 15.37 11.79 11.79 0 0 0 4.1.73 12 12 0 0 0 11.28-7.9l64-176a12 12 0 0 0-7.18-15.38Z'/%3E%3C/svg%3E");
}
.navbar-item-sdks::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='m225.6 62.64-88-48.17a19.91 19.91 0 0 0-19.2 0l-88 48.17A20 20 0 0 0 20 80.19v95.62a20 20 0 0 0 10.4 17.55l88 48.17a19.89 19.89 0 0 0 19.2 0l88-48.17a20 20 0 0 0 10.4-17.55V80.19a20 20 0 0 0-10.4-17.55ZM128 36.57 200 76l-72 39.42L56 76ZM44 96.82l72 39.43v76.89l-72-39.42Zm96 116.32v-76.89l72-39.43v76.89Z'/%3E%3C/svg%3E");
}
.navbar-item-api::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M180.49 143.51a12 12 0 0 1 0 17l-24 24a12 12 0 0 1-17-17L155 152l-15.52-15.51a12 12 0 0 1 17-17ZM112.49 120.49a12 12 0 0 0-17 0l-24 24a12 12 0 0 0 0 17l24 24a12 12 0 0 0 17-17L97 153l15.52-15.51a12 12 0 0 0-.03-17ZM220 88v24a12 12 0 0 1-24 0v-16h-44a12 12 0 0 1-12-12V40H60v68a12 12 0 0 1-24 0V40a20 20 0 0 1 20-20h96a12 12 0 0 1 8.49 3.52l56 56A12 12 0 0 1 220 88Zm-60-8h23L160 57Zm-4 132H60v-12a12 12 0 0 0-24 0v12a20 20 0 0 0 20 20h100a12 12 0 0 0 0-24Zm64-44a12 12 0 0 0-12 12v36h-44a12 12 0 0 0 0 24h44a20 20 0 0 0 20-20v-40a12 12 0 0 0-8-12Z'/%3E%3C/svg%3E");
}
.navbar-item-cookbook::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M224 44H160a43.86 43.86 0 0 0-32 13.85A43.86 43.86 0 0 0 96 44H32a20 20 0 0 0-20 20v128a20 20 0 0 0 20 20h64a20 20 0 0 1 20 20 12 12 0 0 0 24 0 20 20 0 0 1 20-20h64a20 20 0 0 0 20-20V64a20 20 0 0 0-20-20ZM96 188H36V68h60a20 20 0 0 1 20 20v108.69A43.74 43.74 0 0 0 96 188Zm124 0h-60a43.74 43.74 0 0 0-20 8.69V88a20 20 0 0 1 20-20h60Z'/%3E%3C/svg%3E");
}
.navbar-item-changelog::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M140 80v41.21l34.17 20.5a12 12 0 1 1-12.34 20.58l-40-24A12 12 0 0 1 116 128V80a12 12 0 0 1 24 0Zm-12-52a99.38 99.38 0 0 0-70.76 29.34c-4.69 4.74-9 9.37-13.24 14V64a12 12 0 0 0-24 0v40a12 12 0 0 0 12 12h40a12 12 0 0 0 0-24H53.41c4.24-5.95 8.53-11.93 13.49-16.95A76 76 0 1 1 52 128a12 12 0 0 0-24 0 100 100 0 1 0 100-100Z'/%3E%3C/svg%3E");
}
/* Dark mode icons */
[data-theme='dark'] .navbar-item-developer::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M71.68 97.22 34.74 128l36.94 30.78a12 12 0 1 1-15.36 18.44l-48-40a12 12 0 0 1 0-18.44l48-40a12 12 0 0 1 15.36 18.44Zm176 21.56-48-40a12 12 0 1 0-15.36 18.44L221.26 128l-36.94 30.78a12 12 0 1 0 15.36 18.44l48-40a12 12 0 0 0 0-18.44ZM164.1 28.72a12 12 0 0 0-15.38 7.18l-64 176a12 12 0 0 0 7.18 15.37 11.79 11.79 0 0 0 4.1.73 12 12 0 0 0 11.28-7.9l64-176a12 12 0 0 0-7.18-15.38Z'/%3E%3C/svg%3E");
}
[data-theme='dark'] .navbar-item-sdks::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='m225.6 62.64-88-48.17a19.91 19.91 0 0 0-19.2 0l-88 48.17A20 20 0 0 0 20 80.19v95.62a20 20 0 0 0 10.4 17.55l88 48.17a19.89 19.89 0 0 0 19.2 0l88-48.17a20 20 0 0 0 10.4-17.55V80.19a20 20 0 0 0-10.4-17.55ZM128 36.57 200 76l-72 39.42L56 76ZM44 96.82l72 39.43v76.89l-72-39.42Zm96 116.32v-76.89l72-39.43v76.89Z'/%3E%3C/svg%3E");
}
[data-theme='dark'] .navbar-item-api::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M180.49 143.51a12 12 0 0 1 0 17l-24 24a12 12 0 0 1-17-17L155 152l-15.52-15.51a12 12 0 0 1 17-17ZM112.49 120.49a12 12 0 0 0-17 0l-24 24a12 12 0 0 0 0 17l24 24a12 12 0 0 0 17-17L97 153l15.52-15.51a12 12 0 0 0-.03-17ZM220 88v24a12 12 0 0 1-24 0v-16h-44a12 12 0 0 1-12-12V40H60v68a12 12 0 0 1-24 0V40a20 20 0 0 1 20-20h96a12 12 0 0 1 8.49 3.52l56 56A12 12 0 0 1 220 88Zm-60-8h23L160 57Zm-4 132H60v-12a12 12 0 0 0-24 0v12a20 20 0 0 0 20 20h100a12 12 0 0 0 0-24Zm64-44a12 12 0 0 0-12 12v36h-44a12 12 0 0 0 0 24h44a20 20 0 0 0 20-20v-40a12 12 0 0 0-8-12Z'/%3E%3C/svg%3E");
}
[data-theme='dark'] .navbar-item-cookbook::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M224 44H160a43.86 43.86 0 0 0-32 13.85A43.86 43.86 0 0 0 96 44H32a20 20 0 0 0-20 20v128a20 20 0 0 0 20 20h64a20 20 0 0 1 20 20 12 12 0 0 0 24 0 20 20 0 0 1 20-20h64a20 20 0 0 0 20-20V64a20 20 0 0 0-20-20ZM96 188H36V68h60a20 20 0 0 1 20 20v108.69A43.74 43.74 0 0 0 96 188Zm124 0h-60a43.74 43.74 0 0 0-20 8.69V88a20 20 0 0 1 20-20h60Z'/%3E%3C/svg%3E");
}
[data-theme='dark'] .navbar-item-changelog::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M140 80v41.21l34.17 20.5a12 12 0 1 1-12.34 20.58l-40-24A12 12 0 0 1 116 128V80a12 12 0 0 1 24 0Zm-12-52a99.38 99.38 0 0 0-70.76 29.34c-4.69 4.74-9 9.37-13.24 14V64a12 12 0 0 0-24 0v40a12 12 0 0 0 12 12h40a12 12 0 0 0 0-24H53.41c4.24-5.95 8.53-11.93 13.49-16.95A76 76 0 1 1 52 128a12 12 0 0 0-24 0 100 100 0 1 0 100-100Z'/%3E%3C/svg%3E");
}
}
/* GitHub icon link */
.header-github-link::before {
@@ -212,11 +153,7 @@
height: 24px !important;
}
/* Hide all desktop navbar items except logo and toggle */
.navbar__items--right > .navbar__item {
display: none !important;
}
/* Hide left navbar links (accessible via hamburger) */
.navbar__items--left > .navbar__link {
display: none !important;
}
@@ -301,6 +238,13 @@
}
}
/* Truly mobile (<= 996px): hide right navbar items too */
@media (max-width: 996px) {
.navbar__items--right > .navbar__item {
display: none !important;
}
}
/* Mobile sidebar - show right navbar items */
@media (max-width: 1399px) {
/* Ensure right-side items are visible in mobile sidebar */
@@ -589,16 +533,6 @@ div[class*="codeBlockContent"] .prism-code {
max-width: 100%;
}
/* Force blog posts to be wider - aggressive override */
body[class*="blog"] .container,
body[class*="blog"] main .container {
max-width: 100% !important;
}
body[class*="blog"] article {
max-width: 1200px !important;
margin: 0 auto !important;
}
/* Page title with gradient */
article h1,
@@ -1336,6 +1270,48 @@ ul[class*="suggestion"] {
display: none !important;
}
/* ===== Blog Post & Cookbook Typography ===== */
/* Shared: larger body text, looser line-height */
html.blog-post-page article p,
html.blog-post-page article li,
html.blog-post-page article blockquote p,
html.mdx-wrapper article p,
html.mdx-wrapper article li,
html.mdx-wrapper article blockquote p,
html.docs-wrapper article p,
html.docs-wrapper article li,
html.docs-wrapper article blockquote p {
font-size: 1rem;
line-height: 1.85;
}
/* Blog only: monospace body font — Supermemory-style */
html.blog-post-page article p,
html.blog-post-page article li,
html.blog-post-page article blockquote p {
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
}
/* Shared: bump heading sizes */
html.blog-post-page article h1,
html.mdx-wrapper article h1,
html.docs-wrapper article h1 {
font-size: 2.5rem;
}
html.blog-post-page article h2,
html.mdx-wrapper article h2,
html.docs-wrapper article h2 {
font-size: 1.6rem;
}
html.blog-post-page article h3,
html.mdx-wrapper article h3,
html.docs-wrapper article h3 {
font-size: 1.2rem;
}
/* ===== Blog Styling ===== */
/* Blog author text visible in light mode */
@@ -1384,75 +1360,3 @@ ul[class*="suggestion"] {
}
}
/* ============================================
Cookbook: Hide sidebar for OpenAI-style layout
============================================ */
/* Hide sidebar completely on cookbook pages - use multiple selectors for reliability */
[class*="docPage"] aside[class*="docSidebarContainer"],
aside[class*="docSidebarContainer"]:has(+ * .cookbook-page),
body:has(.cookbook-page) aside[class*="docSidebarContainer"],
.hidden-sidebar aside,
article[id="cookbook-index"] ~ aside,
div:has(> article[id="cookbook-index"]) aside {
display: none !important;
width: 0 !important;
min-width: 0 !important;
}
/* Make main wrapper full width */
body:has(.cookbook-page) .main-wrapper,
.hidden-sidebar ~ * .main-wrapper,
div:has(> article[id="cookbook-index"]) {
max-width: 100% !important;
}
/* Make doc page container full width */
body:has(.cookbook-page) [class*="docMainContainer"],
.hidden-sidebar [class*="docMainContainer"],
div:has(> .cookbook-page) > div {
max-width: 100% !important;
}
/* Make the content column full width */
body:has(.cookbook-page) [class*="docItemCol"],
.hidden-sidebar [class*="docItemCol"],
.cookbook-page ~ * [class*="col"] {
max-width: 100% !important;
flex: 1 1 100% !important;
}
/* Container adjustments */
.cookbook-page .container,
body:has(.cookbook-page) .container {
max-width: 1400px !important;
padding-left: 2rem !important;
padding-right: 2rem !important;
}
/* Responsive adjustments */
@media (max-width: 996px) {
.cookbook-page .container {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
}
/* Additional fallback selectors for hiding cookbook sidebar */
[data-route="/cookbook"] aside,
[data-route="/cookbook/"] aside,
div[class*="docPage"]:has(article[id*="cookbook"]) > aside:first-child {
display: none !important;
}
/* Force full width on cookbook route */
[data-route="/cookbook"] div[class*="docRoot"],
[data-route="/cookbook/"] div[class*="docRoot"] {
grid-template-columns: 0 auto !important;
}
[data-route="/cookbook"] main,
[data-route="/cookbook/"] main {
max-width: 100% !important;
}
@@ -1,25 +1,32 @@
---
sidebar_position: 1
title: Cookbook
hide_table_of_contents: true
pagination_next: null
pagination_prev: null
custom_edit_url: null
sidebar_class_name: hidden-sidebar
---
import RecipeCarousel from '@site/src/components/RecipeCarousel';
import CookbookGrid from '@site/src/components/CookbookGrid';
<div className="cookbook-page">
<div>
# Cookbook
<div style={{textAlign: 'center', marginBottom: '3.5rem'}}>
<h1 style={{
fontSize: '3rem',
fontWeight: 800,
background: 'linear-gradient(135deg, #0074d9, #009296)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
letterSpacing: '-0.03em',
lineHeight: 1.15,
marginBottom: '0.75rem',
}}>Cookbook</h1>
<p style={{fontSize: '1.05rem', color: 'var(--ifm-color-emphasis-600)', maxWidth: 520, margin: '0 auto', lineHeight: 1.7}}>
Practical examples and complete applications built with Hindsight.
</p>
</div>
Learn how to build with Hindsight through practical examples:
## Recipes
- **[Recipes](#recipes)** - Step-by-step guides and patterns for common use cases
- **[Applications](#applications)** - Complete, runnable applications demonstrating Hindsight integration
<RecipeCarousel
title="Recipes"
<CookbookGrid
items={[
{
title: "Hindsight Quickstart",
@@ -90,8 +97,9 @@ Learn how to build with Hindsight through practical examples:
]}
/>
<RecipeCarousel
title="Applications"
## Applications
<CookbookGrid
items={[
{
title: "Chat Memory App",
@@ -0,0 +1,16 @@
import React, {type ReactNode} from 'react';
import Layout from '@theme/Layout';
import type {Props} from '@theme/BlogLayout';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function BlogLayout({sidebar: _sidebar, toc: _toc, children, ...layoutProps}: Props): ReactNode {
return (
<Layout {...layoutProps}>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">{children}</main>
</div>
</div>
</Layout>
);
}
@@ -0,0 +1,78 @@
import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import type {Props} from '@theme/BlogListPage';
import type {PropBlogPostContent} from '@docusaurus/plugin-content-blog';
import styles from './styles.module.css';
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric'});
}
function BlogCard({content}: {content: PropBlogPostContent}) {
const {metadata, assets} = content;
const {title, description, date, readingTime, permalink, frontMatter} = metadata;
const image = assets.image ?? frontMatter.image ?? '/img/blog-default.jpg';
return (
<Link to={permalink} className={styles.card}>
<div className={styles.cardImageWrapper}>
{image ? (
<img src={image} alt={title} className={styles.cardImage} />
) : (
<div className={styles.cardImagePlaceholder} />
)}
</div>
<div className={styles.cardBody}>
<h2 className={styles.cardTitle}>{title}</h2>
{description && <p className={styles.cardDescription}>{description}</p>}
<div className={styles.cardFooter}>
<span className={styles.cardDate}>{formatDate(date)}</span>
{readingTime !== undefined && (
<span className={styles.cardReadTime}>{Math.ceil(readingTime)} min read</span>
)}
</div>
</div>
</Link>
);
}
export default function BlogListPage({items, metadata}: Props): React.ReactElement {
const {blogTitle, blogDescription, totalPages, page, nextPage, previousPage} = metadata;
return (
<Layout title={blogTitle} description={blogDescription}>
<main className={styles.blogPage}>
<header className={styles.header}>
<h1 className={styles.headerTitle}>{blogTitle}</h1>
{blogDescription && <p className={styles.headerSubtitle}>{blogDescription}</p>}
</header>
<div className={styles.grid}>
{items.map(({content: BlogPostContent}) => (
<BlogCard key={BlogPostContent.metadata.permalink} content={BlogPostContent} />
))}
</div>
{totalPages > 1 && (
<nav className={styles.pagination}>
{previousPage && (
<Link to={previousPage} className={styles.paginationButton}>
Previous
</Link>
)}
<span className={styles.paginationInfo}>
Page {page} of {totalPages}
</span>
{nextPage && (
<Link to={nextPage} className={styles.paginationButton}>
Next
</Link>
)}
</nav>
)}
</main>
</Layout>
);
}
@@ -0,0 +1,185 @@
.blogPage {
max-width: 1280px;
margin: 0 auto;
padding: 4rem 2rem 6rem;
}
/* ── Header ─────────────────────────────────────── */
.header {
text-align: center;
margin-bottom: 4rem;
}
.headerTitle {
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, #0074d9, #009296);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1.15;
letter-spacing: -0.03em;
}
.headerSubtitle {
font-size: 1.05rem;
color: var(--ifm-color-emphasis-600);
max-width: 520px;
margin: 0 auto;
line-height: 1.7;
}
/* ── Grid ────────────────────────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
@media (max-width: 996px) {
.grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr;
gap: 2rem;
}
.headerTitle {
font-size: 2.25rem;
}
}
/* ── Card ────────────────────────────────────────── */
.card {
display: flex;
flex-direction: column;
text-decoration: none !important;
color: inherit;
border-radius: 0;
background: transparent;
transition: opacity 0.2s ease;
}
.card:hover {
text-decoration: none !important;
color: inherit;
opacity: 0.9;
}
.card:hover .cardImage {
transform: none;
}
/* ── Card image ──────────────────────────────────── */
.cardImageWrapper {
aspect-ratio: 16 / 9;
overflow: hidden;
width: 100%;
border-radius: 4px;
background: #111;
}
.cardImage {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.4s ease;
}
.cardImagePlaceholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0074d9 0%, #009296 100%);
opacity: 0.35;
}
/* ── Card body ───────────────────────────────────── */
.cardBody {
display: flex;
flex-direction: column;
flex: 1;
padding: 1rem 0 0;
gap: 0.5rem;
}
.cardTitle {
font-size: 1.25rem;
font-weight: 700;
line-height: 1.35;
margin: 0;
color: var(--ifm-heading-color);
letter-spacing: -0.02em;
}
.cardDescription {
font-size: 0.85rem;
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
color: var(--ifm-color-emphasis-500);
line-height: 1.65;
margin: 0;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.cardFooter {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.5rem;
}
.cardDate {
font-size: 0.78rem;
color: var(--ifm-color-emphasis-400);
font-weight: 500;
}
.cardReadTime {
font-size: 0.78rem;
color: var(--ifm-color-emphasis-400);
}
.cardReadTime::before {
content: '·';
margin-right: 0.35rem;
}
/* ── Pagination ──────────────────────────────────── */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin-top: 4rem;
}
.paginationButton {
padding: 0.5rem 1.25rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--ifm-color-primary);
text-decoration: none !important;
transition: background 0.15s ease, border-color 0.15s ease;
}
.paginationButton:hover {
background: var(--ifm-color-primary-lightest);
border-color: var(--ifm-color-primary);
}
.paginationInfo {
font-size: 0.875rem;
color: var(--ifm-color-emphasis-600);
}
@@ -2,20 +2,9 @@ import React from 'react';
import DocItemContent from '@theme-original/DocItem/Content';
import type DocItemContentType from '@theme/DocItem/Content';
import type { WrapperProps } from '@docusaurus/types';
import CopyPageButton from '@site/src/components/CopyPageButton';
import styles from './styles.module.css';
type Props = WrapperProps<typeof DocItemContentType>;
export default function DocItemContentWrapper(props: Props): JSX.Element {
return (
<>
<div className={styles.docItemHeader}>
<div className={styles.docItemActions}>
<CopyPageButton />
</div>
</div>
<DocItemContent {...props} />
</>
);
}
return <DocItemContent {...props} />;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

@@ -1,171 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
<title>Easy to Use</title>
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/>
<path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/>
<path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/>
<path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/>
<circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/>
<circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/>
<circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/>
<circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/>
<circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/>
<circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/>
<path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/>
<path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/>
<path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/>
<path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/>
<path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/>
<rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/>
<path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(312.271 493.733)">
<path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)">
<path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)">
<path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(10.671 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 31 KiB

@@ -1,170 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
<title>Powered by React</title>
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
<path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/>
<path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/>
<path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/>
<rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/>
<rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/>
<path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/>
<path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/>
<path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/>
<path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/>
<path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/>
<path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/>
<circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/>
<path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/>
<path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/>
<path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/>
<path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/>
<path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/>
<path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(670.271 615.768)">
<path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)">
<path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)">
<path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(5.073 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/>
</g>
<g id="React-icon" transform="translate(906.3 541.56)">
<path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/>
<path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/>
<circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/>
<path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

@@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
<title>Focus on What Matters</title>
<circle cx="321" cy="321" r="321" fill="#f2f2f2" />
<ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
<ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
<rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
<path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
<circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
<rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
<rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
<rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
<path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
<path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
<path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
<path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -1,315 +0,0 @@
---
sidebar_position: 1
---
# OpenAI Agent + Hindsight Memory Integration
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/openai-fitness-coach)
:::
A fitness coach example demonstrating how to use **OpenAI Agents** with **Hindsight as a memory backend**.
## What This Demonstrates
This example showcases:
- **OpenAI Assistants** handling conversation logic
- **Hindsight** providing sophisticated memory storage & retrieval
- **Function calling** to bridge them together
- **Streaming responses** for real-time interaction (enabled by default)
- **Bidirectional memory** - both user data AND coach observations stored
- **System-level post-processing** - automatic opinion storage for reliability
- **Temporal-semantic memory** queries via function tools
- **Enhanced preference learning** - coach learns and respects user likes/dislikes
- **Real-world integration pattern** for adding memory to AI agents
## Architecture
```
User: "I ran 5K today, don't like tempo runs"
|
OpenAI Assistant
|
Function Call: store_memory(workout + preference)
|
Hindsight API (stores as world/agent)
|
OpenAI Assistant: "What should I focus on?"
|
Function Call: retrieve_memories("workouts and preferences")
|
Hindsight API (returns workouts + preferences)
|
OpenAI Assistant (analyzes, gives advice)
|
Function Call: store_memory(advice as opinion)
|
Hindsight API (stores coach's observation)
|
Personalized Answer
```
## Key Difference from Standard Demo
| Component | Standard Demo | OpenAI Integration |
|-----------|---------------|-------------------|
| **Conversation** | Hindsight `/think` endpoint | OpenAI Assistant API |
| **Memory** | Hindsight (built-in) | Hindsight (via function calling) |
| **LLM** | Configured in Hindsight | OpenAI GPT-4 |
| **Opinion Formation** | Automatic in `/think` | Explicit via `store_memory(type="opinion")` |
| **Best For** | Hindsight-native apps | Integrating memory into existing OpenAI agents |
## Quick Start
### Prerequisites
1. **OpenAI API Key**
```bash
export OPENAI_API_KEY=your_openai_api_key
```
2. **Hindsight API running**
```bash
# Follow Hindsight setup instructions to start the API
# Default: http://localhost:8888
```
3. **Install dependencies**
```bash
pip install openai requests
```
### Run the Conversational Demo
```bash
cd openai-fitness-coach
export OPENAI_API_KEY=your_key_here
python demo_conversational.py
```
The demo showcases:
1. **Natural language workout logging** - Tell the coach what you did conversationally
2. **Preference learning** - Express likes/dislikes and watch the coach adapt
3. **Goal tracking** - Set goals, track progress, achieve milestones
4. **Bidirectional memory** - Both your activities AND coach's advice are stored
5. **Streaming responses** - See responses appear in real-time
6. **7 interactive phases** - From goal setting to achievement recognition
The demo uses a separate agent (`fitness-coach-demo`) to avoid mixing with real data.
## Usage
### Chat with Your Coach
**Interactive mode:**
```bash
python openai_coach.py
```
**Single question:**
```bash
python openai_coach.py "What did I do for training this week?"
```
## How It Works
### 1. Memory Tools (`memory_tools.py`)
Defines function tools that the OpenAI Agent can call:
```python
retrieve_memories(query, fact_types, top_k)
search_workouts(after_date, before_date, workout_type)
get_nutrition_summary(after_date, before_date)
get_user_goals()
get_coach_opinions(about)
```
Each function makes API calls to Hindsight to fetch relevant memories.
### 2. OpenAI Agent (`openai_coach.py`)
Creates an OpenAI Assistant with:
- Fitness coaching instructions
- Access to memory function tools
- Conversation management
When you ask a question:
1. User message is sent to OpenAI Assistant
2. Assistant decides which memory functions to call
3. Functions fetch data from Hindsight
4. Assistant generates response using retrieved context
### 3. Function Calling Flow
```python
# User asks: "What did I run this week?"
# OpenAI Assistant decides to call:
search_workouts(
after_date="2024-11-18",
workout_type="running"
)
# Function retrieves from Hindsight:
{
"results": [
{"text": "User completed 45-minute cardio workout: running..."},
{"text": "User completed 60-minute cardio workout: running..."}
]
}
# OpenAI Assistant generates response:
"This week you've done two runs: a 45-minute run on Monday
and a longer 60-minute run on Wednesday. Great consistency!"
```
## Example Questions
Try asking:
```bash
python openai_coach.py "What does my training look like this week?"
python openai_coach.py "Based on my workouts, should I rest today?"
python openai_coach.py "How is my nutrition supporting my goals?"
python openai_coach.py "What's my progress toward my goal?"
python openai_coach.py "Compare my training this month to last month"
```
The agent will automatically:
1. Identify what memories it needs
2. Call the appropriate function tools
3. Retrieve data from Hindsight
4. Generate a personalized response
## Memory Types Retrieved
The OpenAI Agent can retrieve different memory types from Hindsight:
- **World Facts** (`fact_type: "world"`): Workouts, meals, activities
- **Agent Facts** (`fact_type: "agent"`): Goals, intentions
- **Opinions** (`fact_type: "opinion"`): Coach's observations about patterns
## Customization
### Add New Function Tools
Edit `memory_tools.py` to add new capabilities:
```python
def get_weekly_summary(week_offset: int = 0):
"""Get a summary of a specific week."""
# Implementation
pass
# Add to MEMORY_TOOLS list
MEMORY_TOOLS.append({
"type": "function",
"function": {
"name": "get_weekly_summary",
"description": "Get training summary for a specific week",
# ... parameters
}
})
# Add to FUNCTION_MAP
FUNCTION_MAP["get_weekly_summary"] = get_weekly_summary
```
### Modify Assistant Instructions
Edit `openai_coach.py` to change the coach's personality or behavior:
```python
assistant = client.beta.assistants.create(
name="Your Custom Coach",
instructions="Your custom instructions here...",
model="gpt-4o-mini",
tools=MEMORY_TOOLS
)
```
## Use Cases
This pattern works for any application that needs memory:
1. **Customer Support Agents** - Remember past conversations and issues
2. **Personal Assistants** - Remember preferences, schedules, past decisions
3. **Educational Tutors** - Track learning progress over time
4. **Health Coaches** - Monitor habits, progress, goals (like this example)
5. **Sales Assistants** - Remember customer interactions and preferences
## Integration Pattern
**To add Hindsight memory to your own OpenAI Agent:**
1. Define function tools that call Hindsight API
2. Register them with your OpenAI Assistant
3. Implement function handlers to execute Hindsight queries
4. Let OpenAI Assistant decide when to retrieve memories
The key benefit: **Separation of concerns**
- OpenAI = Conversation logic
- Hindsight = Memory storage, retrieval, temporal queries, entity linking
## When to Use This vs. Standard Hindsight
**Use OpenAI + Hindsight (this example) when:**
- You want OpenAI's conversation capabilities
- You're already using OpenAI Agents
- You want explicit control over when to retrieve memories
- You want to combine Hindsight with other OpenAI features
**Use Hindsight directly when:**
- You want a complete memory-first solution
- You want automatic memory retrieval and opinion formation
- You want to use different LLM providers (not just OpenAI)
- You want the `/think` endpoint's integrated approach
## Learning Points
After running this demo, you'll understand:
1. How to add sophisticated memory to any OpenAI Agent
2. How function calling bridges LLMs and memory systems
3. How temporal-semantic queries work via function tools
4. Real-world pattern for LLM + memory integration
## Core Files
- `demo_conversational.py` - Conversational demo showcasing preference learning and goal tracking
- `openai_coach.py` - OpenAI Assistant wrapper with streaming and memory integration
- `memory_tools.py` - Function calling tools that bridge to Hindsight API
- `.openai_assistant_id` - Saved assistant ID (auto-generated, gitignored)
## Common Issues
**"OPENAI_API_KEY not set"**
```bash
export OPENAI_API_KEY=your_api_key_here
```
**"Agent not found"**
- Make sure the Hindsight fitness-coach agent exists
**"Connection refused"**
- Make sure Hindsight API is running on localhost:8888
## Next Steps
1. Run the demo to see it in action
2. Try chatting with the coach: `python openai_coach.py`
3. Log your own workouts and meals
4. Experiment with different questions
5. Add custom function tools for your use case
---
**Built with:**
- OpenAI Assistants API
- Hindsight (temporal-semantic memory)
- Function calling for integration
@@ -1,27 +0,0 @@
---
sidebar_position: 1
---
import RecipeCarousel from '@site/src/components/RecipeCarousel';
# Cookbook
Practical patterns, recipes, and complete applications for building with Hindsight.
<RecipeCarousel
title="Recipes"
items={[
{ title: "Hindsight Quickstart", href: "/cookbook/recipes/quickstart" },
{ title: "Per-User Memory", href: "/cookbook/recipes/per-user-memory" },
{ title: "Support Agent with Shared Knowledge", href: "/cookbook/recipes/support-agent-shared-knowledge" },
{ title: "Memory with LiteLLM", href: "/cookbook/recipes/litellm-memory-demo" },
{ title: "Routing Tool Learning", href: "/cookbook/recipes/tool-learning-demo" }
]}
/>
<RecipeCarousel
title="Applications"
items={[
{ title: "OpenAI Agent + Hindsight Memory Integration", href: "/cookbook/applications/openai-fitness-coach" }
]}
/>
@@ -1,187 +0,0 @@
---
sidebar_position: 4
---
# Memory with LiteLLM
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/04-litellm-memory-demo.ipynb)
:::
This notebook demonstrates how to add persistent memory to any LLM app using the `hindsight-litellm` package. Memory storage and injection happen automatically via LiteLLM callbacks - no manual memory management needed!
**Key features demonstrated:**
1. `configure()` + `enable()` - Set up automatic memory integration
2. Automatic storage - Conversations are stored after each LLM call
3. Automatic injection - Relevant memories are injected into prompts
The `hindsight-litellm` package hooks into LiteLLM's callback system to:
- Store each conversation after successful LLM responses
- Inject relevant memories into the system prompt before LLM calls
## Prerequisites
Make sure you have Hindsight running:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- API: http://localhost:8888
- UI: http://localhost:9999
## Installation
```python
!pip install hindsight-litellm litellm nest_asyncio python-dotenv -U -q
```
## Setup
```python
import os
import uuid
import time
import logging
import nest_asyncio
from dotenv import load_dotenv
# Apply nest_asyncio for Jupyter compatibility
nest_asyncio.apply()
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Proxy").setLevel(logging.WARNING)
# Import hindsight_litellm
import hindsight_litellm
# Configuration
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
# Check for API key
if not os.getenv("OPENAI_API_KEY"):
print("Warning: OPENAI_API_KEY not set")
```
## Configure and Enable Automatic Memory
This is all you need! After this, all LiteLLM calls will automatically:
- Have relevant memories injected into the prompt
- Store conversations to Hindsight after the response
```python
# Generate a unique bank_id for this demo session
bank_id = f"demo-{uuid.uuid4().hex[:8]}"
print(f"Using bank_id: {bank_id}")
# Configure and enable hindsight
hindsight_litellm.configure(
hindsight_api_url=HINDSIGHT_API_URL,
bank_id=bank_id,
store_conversations=True, # Automatically store conversations
inject_memories=True, # Automatically inject relevant memories
verbose=True, # Enable logging to debug memory operations
)
hindsight_litellm.enable()
print("Hindsight memory integration enabled!")
```
## Conversation 1: User Introduces Themselves
In this first conversation, the user shares some information about themselves. This will be automatically stored to Hindsight memory.
```python
user_message_1 = "Hi! I'm Alex and I work at Google as a software engineer. I love Python and machine learning."
print(f"User: {user_message_1}\n")
# Use hindsight_litellm.completion() directly
response_1 = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message_1}
],
)
assistant_response_1 = response_1.choices[0].message.content
print(f"Assistant: {assistant_response_1}")
print("\n(Conversation automatically stored to Hindsight)")
```
## Wait for Memory Processing
Hindsight needs a few seconds to process and extract facts from the conversation.
```python
print("Waiting 12 seconds for memory processing...")
time.sleep(12)
print("Done!")
```
## Conversation 2: Test Memory-Augmented Response
Now we start a fresh conversation and ask what the assistant remembers. The memories from the previous conversation will be automatically injected into the prompt!
```python
user_message_2 = "What do you know about me? What programming language should I use for my next project?"
print(f"User: {user_message_2}\n")
# Memories are automatically injected before this call!
response_2 = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message_2}
],
)
print(f"Assistant: {response_2.choices[0].message.content}")
```
## Summary
The assistant should have remembered that Alex:
- Works at Google as a software engineer
- Loves Python and machine learning
And it should have recommended Python based on that knowledge!
```python
print(f"Memories stored in bank: {bank_id}")
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
```
## Cleanup
```python
hindsight_litellm.cleanup()
# Optional: delete the bank
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted bank: {response.json()}")
```
@@ -1,247 +0,0 @@
---
sidebar_position: 2
---
# Per-User Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/02-per-user-memory.ipynb)
:::
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, user preferences, and context across sessions.
## The Problem
Without memory, every conversation starts from scratch:
```
Session 1: "I prefer dark mode and use Python"
Session 2: "What's my preferred language?" → Agent doesn't know
```
## The Solution: One Bank Per User
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ User B Bank │ │ User C Bank │
│ │ │ │ │ │
│ - Conversations│ │ - Conversations│ │ - Conversations│
│ - Preferences │ │ - Preferences │ │ - Preferences │
│ - Context │ │ - Context │ │ - Context │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
100% isolated 100% isolated 100% isolated
```
Each user gets their own memory bank. Complete isolation, simple mental model.
```python
!pip install hindsight-client nest_asyncio openai python-dotenv -U
```
## 1. Create a Bank When User Signs Up
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
from openai import OpenAI as OpenAIClient
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
def on_user_signup(user_id: str):
client.create_bank(
bank_id=f"user-{user_id}",
name=f"Memory for {user_id}"
)
print(f"View bank: {HINDSIGHT_UI_URL}/banks/user-{user_id}?view=documents")
```
## 2. Manage Conversation Sessions
Use `document_id` to group messages belonging to the same conversation. When you retain with the same `document_id`, Hindsight replaces the previous version (upsert behavior), keeping the memory up-to-date as the conversation evolves.
```python
import uuid
import json
class ConversationSession:
def __init__(self, user_id: str):
self.user_id = user_id
self.session_id = str(uuid.uuid4()) # Unique ID for this conversation
self.messages = []
def add_message(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
def save(self, client: Hindsight):
"""Save the entire conversation. Replaces previous version if session_id exists."""
# Convert messages to string format for retain
content = "\n".join([f"{m['role']}: {m['content']}" for m in self.messages])
client.retain(
bank_id=f"user-{self.user_id}",
content=content,
document_id=self.session_id # Same ID = upsert (replace old version)
)
```
## 3. Recall Context Before Responding
```python
def get_context(user_id: str, query: str):
result = client.recall(
bank_id=f"user-{user_id}",
query=query
)
return result.results
```
## 4. Complete Agent Loop
```python
def format_results(results):
"""Format recall results for the prompt."""
if not results:
return "No relevant memories found."
return "\n".join([f"- {r.text}" for r in results])
def format_messages(messages):
"""Format conversation messages for the prompt."""
return "\n".join([f"{m['role']}: {m['content']}" for m in messages])
def handle_message(session: ConversationSession, user_message: str):
# 1. Add user message to session
session.add_message("user", user_message)
# 2. Recall relevant context from past conversations
context = client.recall(
bank_id=f"user-{session.user_id}",
query=user_message
)
# 3. Build system prompt with memory
system_prompt = f"""You are a helpful assistant with memory of past conversations.
## What you remember about this user
{format_results(context.results)}
Respond helpfully and reference relevant memories when appropriate."""
# 4. Generate response using OpenAI
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
*[{"role": m["role"], "content": m["content"]} for m in session.messages]
]
)
assistant_response = response.choices[0].message.content
# 5. Add assistant response to session
session.add_message("assistant", assistant_response)
# 6. Save the updated conversation (upserts based on session_id)
session.save(client)
print(f"User: {user_message}")
print(f"Assistant: {assistant_response}\n")
return assistant_response
```
## 5. Starting a New Conversation
```python
# Create the user's bank
on_user_signup("alice")
# Each new conversation gets a new session with a unique ID
session = ConversationSession(user_id="alice")
# Multiple exchanges in the same conversation
handle_message(session, "Hi! I'm working on a Python project")
handle_message(session, "Can you help me with async/await?")
# View the stored conversation in the UI.
# Each message updates the same document (via document_id), so you'll see
# the full conversation history in a single document rather than separate entries.
print(f"\nView documents: {HINDSIGHT_UI_URL}/banks/user-alice?view=documents")
```
## How Document ID Works
The `document_id` parameter is key to managing evolving conversations:
| Scenario | Behavior |
|----------|----------|
| First retain with `document_id="session_123"` | Creates new document |
| Retain again with same `document_id="session_123"` | **Replaces** previous version (upsert) |
| Retain with different `document_id="session_456"` | Creates separate document |
| Retain without `document_id` | Creates new document each time |
This upsert behavior means:
- You always retain the **full conversation** state
- Facts are re-extracted from the complete conversation
- No duplicate or stale facts from old versions
- Memory stays consistent as conversations evolve
## What Gets Remembered
Hindsight automatically extracts and connects:
- **Facts**: "User prefers Python", "User is building a CLI tool"
- **Entities**: People, projects, technologies mentioned
- **Relationships**: How entities relate to each other
- **Temporal context**: When things happened
You don't need to manually extract or structure this - just retain the conversations.
## When to Use This Pattern
**Good fit:**
- Chatbots and assistants
- Personal AI companions
- Any 1:1 user-to-agent interaction
**Consider adding shared knowledge if:**
- You have product docs or FAQs to reference
- Multiple users need access to the same information
- See the Support Agent with Shared Knowledge notebook
## Cleanup
Delete the banks created during this notebook:
```python
import requests
# Delete the user-alice bank
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/user-alice")
print(f"Deleted user-alice: {response.json()}")
```
@@ -1,162 +0,0 @@
---
sidebar_position: 1
---
# Hindsight Quickstart
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/01-quickstart.ipynb)
:::
This notebook covers the basics of using Hindsight:
- **Retain**: Store information in memory
- **Recall**: Retrieve memories matching a query
- **Reflect**: Generate insights from memories
## Prerequisites
Make sure you have Hindsight running. The easiest way is via Docker:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- API: http://localhost:8888
- UI: http://localhost:9999
## Installation
Install the Hindsight Python client:
```python
!pip install hindsight-client nest_asyncio python-dotenv -U
```
## Connect to Hindsight
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
```
## Retain: Store Information
The `retain` operation is used to push new memories into Hindsight. It tells Hindsight to _retain_ the information you pass in.
Behind the scenes, the retain operation uses an LLM to extract key facts, temporal data, entities, and relationships.
```python
# Simple retain
client.retain(
bank_id="my-bank",
content="Alice works at Google as a software engineer"
)
# View the stored document in the UI:
print(f"View documents: {HINDSIGHT_UI_URL}/banks/my-bank?view=documents")
```
```python
# Retain with context and timestamp
client.retain(
bank_id="my-bank",
content="Alice got promoted to senior engineer",
context="career update",
timestamp="2025-06-15T10:00:00Z"
)
```
## Recall: Retrieve Memories
The `recall` operation retrieves memories matching a query. It performs 4 retrieval strategies in parallel:
- **Semantic**: Vector similarity
- **Keyword**: BM25 exact matching
- **Graph**: Entity/temporal/causal links
- **Temporal**: Time range filtering
```python
# Simple recall
results = client.recall(bank_id="my-bank", query="What does Alice do?")
print("Memories:")
for r in results.results:
print(f" - {r.text}")
```
```python
# Temporal recall
results = client.recall(bank_id="my-bank", query="What happened in June?")
print("Memories:")
for r in results.results:
print(f" - {r.text}")
```
## Reflect: Generate Insights
The `reflect` operation performs a more thorough analysis of existing memories. This allows the agent to form new connections between memories which are then persisted as opinions and/or observations.
Example use cases:
- An AI Project Manager reflecting on what risks need to be mitigated
- A Sales Agent reflecting on why certain outreach messages have gotten responses
- A Support Agent reflecting on opportunities where customers have unanswered questions
```python
response = client.reflect(bank_id="my-bank", query="What should I know about Alice?")
print(response)
```
## Memory Types
Hindsight organizes memory into four networks to mimic human memory:
- **World**: Facts about the world ("The stove gets hot")
- **Experiences**: Agent's own experiences ("I touched the stove and it really hurt")
- **Opinion**: Beliefs with confidence scores ("I shouldn't touch the stove again" - .99 confidence)
- **Observation**: Complex mental models derived by reflecting on facts and experiences
## Cleanup
Delete the bank created during this notebook:
```python
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/my-bank")
print(f"Deleted my-bank: {response.json()}")
```
@@ -1,315 +0,0 @@
---
sidebar_position: 3
---
# Support Agent with Shared Knowledge
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/03-support-agent-shared-knowledge.ipynb)
:::
This pattern shows how to build a support agent that combines **per-user memory** with **shared product knowledge** (RAG), giving users personalized support while leveraging a single source of truth for documentation.
## The Problem
You're building a support agent that needs to:
- Remember each user's history, preferences, and past issues
- Access shared product documentation
- Keep user data completely isolated from other users
A naive approach would index product docs into each user's memory bank, but this is expensive and wasteful (N copies for N users).
## The Solution: Multi-Bank Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ User B Bank │ │ Shared Docs │
│ │ │ │ │ Bank │
│ - Conversations│ │ - Conversations│ │ │
│ - Preferences │ │ - Preferences │ │ - Product docs │
│ - Past issues │ │ - Past issues │ │ - FAQs │
│ - Solutions │ │ - Solutions │ │ - Guides │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴───────────────────────┘
Agent queries
multiple banks
```
**Key benefits:**
- Product docs indexed once, shared by all users
- User memory is 100% isolated
- Simple mental model, no complex filtering
```python
!pip install hindsight-client nest_asyncio openai python-dotenv -U
```
## 1. Set Up Memory Banks
Create three types of banks:
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
from openai import OpenAI as OpenAIClient
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
# Shared knowledge bank (created once)
shared_bank = client.create_bank(
bank_id="product-docs",
name="Product Documentation"
)
# Per-user banks (created when user signs up)
def create_user_bank(user_id: str):
return client.create_bank(
bank_id=f"user-{user_id}",
name=f"Memory for {user_id}"
)
```
## 2. Index Product Documentation
Index your product docs into the shared bank (do this once, or on doc updates):
```python
# Index product documentation - retain each doc separately
client.retain(
bank_id="product-docs",
content="# Pricing Tiers\n\nBasic: $10/mo, Pro: $25/mo, Enterprise: Contact us"
)
client.retain(
bank_id="product-docs",
content="# Getting Started\n\nTo set up your account, visit the dashboard and click 'New Project'"
)
# View the stored documents in the UI:
print(f"View documents: {HINDSIGHT_UI_URL}/banks/product-docs?view=documents")
```
## 3. Store User Conversations
After each support interaction, retain it in the user's bank:
```python
def save_conversation(user_id: str, messages: list):
# Convert messages to string format
content = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
client.retain(
bank_id=f"user-{user_id}",
content=content
)
```
## 4. Query Multiple Banks at Support Time
When handling a user query, retrieve context from both banks:
```python
def get_support_context(user_id: str, query: str):
# Get user's personal context
user_context = client.recall(
bank_id=f"user-{user_id}",
query=query
)
# Get relevant product documentation
docs_context = client.recall(
bank_id="product-docs",
query=query
)
return {
"user_history": user_context.results,
"documentation": docs_context.results
}
```
## 5. Build the Agent Prompt
Combine both contexts in your agent's prompt:
```python
def format_results(results):
"""Format recall results for the prompt."""
if not results:
return "No relevant information found."
return "\n".join([f"- {r.text}" for r in results])
def build_prompt(query: str, context: dict) -> str:
return f"""You are a helpful support agent.
## User's History
{format_results(context["user_history"])}
## Product Documentation
{format_results(context["documentation"])}
## Current Question
{query}
Use the user's history to personalize your response and the documentation
for accurate product information. If you find a solution, remember it for
future reference.
"""
```
## Promoting Learnings to Shared Knowledge
When the agent discovers a solution that's not in the docs, you can optionally promote it to a "learnings" bank:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ Shared Docs │ │ Learnings │
│ │ │ Bank │ │ Bank │
│ - Conversations│ │ │ │ │
│ - Preferences │ │ - Product docs │ │ - Verified │
│ - Past issues │ │ - FAQs │ │ solutions │
│ - Solutions │ │ - Guides │ │ - Workarounds │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴───────────────────────┘
Agent queries
all three banks
```
```python
# Optional: Create a curated learnings bank
learnings_bank = client.create_bank(
bank_id="support-learnings",
name="Curated Support Learnings"
)
# After a successful resolution
def promote_learning(insight: str):
client.retain(
bank_id="support-learnings",
content=insight
)
```
## Complete Example
```python
def format_results(results):
if not results:
return "No relevant information found."
return "\n".join([f"- {r.text}" for r in results])
def handle_support_request(user_id: str, query: str):
# 1. Recall from user's memory
user_recall = client.recall(
bank_id=f"user-{user_id}",
query=query
)
# 2. Recall from shared docs
docs_recall = client.recall(
bank_id="product-docs",
query=query
)
# 3. Recall from learnings (optional)
learnings_recall = client.recall(
bank_id="support-learnings",
query=query
)
# 4. Build system prompt with context
system_prompt = f"""You are a helpful support agent. Use the context below to answer the user's question.
## User's History
{format_results(user_recall.results)}
## Product Documentation
{format_results(docs_recall.results)}
## Known Solutions
{format_results(learnings_recall.results)}
Provide helpful, accurate responses based on the documentation. Reference the user's history when relevant."""
# 5. Generate response using OpenAI
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
]
)
assistant_response = response.choices[0].message.content
# 6. Save the conversation to user's memory
conversation = f"user: {query}\nassistant: {assistant_response}"
client.retain(
bank_id=f"user-{user_id}",
content=conversation
)
return assistant_response
# Test the function
create_user_bank("bob")
print("User: How do I get started?")
result = handle_support_request("bob", "How do I get started?")
print(f"Assistant: {result}")
print(f"\nView user memory: {HINDSIGHT_UI_URL}/banks/user-bob?view=documents")
```
## When to Use This Pattern
**Good fit:**
- Support agents with shared documentation
- Multi-tenant applications with shared reference data
- Any scenario needing user isolation + shared knowledge
**Consider alternatives if:**
- You need cross-user learning (users benefiting from other users' solutions)
- Entity relationships must span across users and docs
## Cleanup
Delete the banks created during this notebook:
```python
import requests
# Delete all banks created in this notebook
for bank_id in ["product-docs", "support-learnings", "user-bob"]:
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted {bank_id}: {response.json()}")
```
@@ -1,372 +0,0 @@
---
sidebar_position: 5
---
# Routing Tool Learning
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/05-tool-learning-demo.ipynb)
:::
This notebook demonstrates how Hindsight helps an LLM learn which tool to use when tool names are ambiguous. Without memory, the LLM might randomly select between similarly-named tools. With Hindsight, it learns from past interactions and consistently makes the correct choice.
## The Scenario
We have a task routing system with two tools:
- `route_to_channel_alpha` - Routes to processing channel Alpha
- `route_to_channel_omega` - Routes to processing channel Omega
The tool names and descriptions are **intentionally vague**. In reality:
- Channel Alpha handles **FINANCIAL/PAYMENT** tasks (refunds, billing, etc.)
- Channel Omega handles **TECHNICAL/SUPPORT** tasks (bugs, features, etc.)
**Without Hindsight:** The LLM guesses randomly based on vague descriptions
**With Hindsight:** The LLM learns from feedback which channel handles what
## Prerequisites
Make sure you have Hindsight running:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## Installation
```python
!pip install hindsight-litellm hindsight-client litellm nest_asyncio python-dotenv -U -q
```
## Setup
```python
import os
import json
import uuid
import time
import logging
import nest_asyncio
from typing import Optional
from dotenv import load_dotenv
nest_asyncio.apply()
load_dotenv()
logging.basicConfig(level=logging.INFO)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
import litellm
import hindsight_litellm
from hindsight_client import Hindsight
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
if not os.getenv("OPENAI_API_KEY"):
print("Warning: OPENAI_API_KEY not set")
```
## Define Tools
These tool definitions are **intentionally ambiguous** - the descriptions don't reveal which channel handles what type of request.
```python
TOOLS = [
{
"type": "function",
"function": {
"name": "route_to_channel_alpha",
"description": "Routes the customer request to processing channel Alpha. Use this channel for appropriate request types.",
"parameters": {
"type": "object",
"properties": {
"request_summary": {
"type": "string",
"description": "A brief summary of the customer's request"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the request"
}
},
"required": ["request_summary"]
}
}
},
{
"type": "function",
"function": {
"name": "route_to_channel_omega",
"description": "Routes the customer request to processing channel Omega. Use this channel for appropriate request types.",
"parameters": {
"type": "object",
"properties": {
"request_summary": {
"type": "string",
"description": "A brief summary of the customer's request"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the request"
}
},
"required": ["request_summary"]
}
}
}
]
```
## Test Scenarios
A mix of financial and technical requests to test routing accuracy.
```python
TEST_SCENARIOS = [
{
"type": "financial",
"request": "I was charged twice for my subscription last month. I need a refund for the duplicate charge.",
"correct_tool": "route_to_channel_alpha"
},
{
"type": "technical",
"request": "The app keeps crashing when I try to upload a file larger than 10MB. This bug is blocking my work.",
"correct_tool": "route_to_channel_omega"
},
{
"type": "financial",
"request": "My invoice shows an incorrect amount. The billing department needs to fix this.",
"correct_tool": "route_to_channel_alpha"
},
{
"type": "technical",
"request": "I'd like to request a new feature: the ability to export reports as PDF.",
"correct_tool": "route_to_channel_omega"
},
{
"type": "financial",
"request": "I need to update my payment method and understand why my last payment failed.",
"correct_tool": "route_to_channel_alpha"
},
]
```
## Helper Functions
```python
SYSTEM_PROMPT = """You are a customer service routing agent. Your job is to route customer requests to the appropriate processing channel.
You have access to two routing channels:
- route_to_channel_alpha: Routes to channel Alpha
- route_to_channel_omega: Routes to channel Omega
Analyze the customer's request and route it to the most appropriate channel. You must call one of the routing functions to process the request.
Important: Base your routing decision on what you know about each channel's purpose. If you have learned from previous interactions which channel handles specific types of requests, use that knowledge."""
def make_routing_request(user_request: str, use_hindsight: bool, bank_id: Optional[str] = None):
"""Make a routing request and return the tool called."""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Customer Request: {user_request}"}
]
if use_hindsight and bank_id:
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="required",
temperature=0.0,
)
else:
response = litellm.completion(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="required",
temperature=0.7,
)
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
return tool_call.function.name
return None
def store_feedback(bank_id: str, request: str, correct_tool: str, request_type: str):
"""Store feedback about which tool was correct for a request type."""
client = Hindsight(base_url=HINDSIGHT_API_URL, timeout=60.0)
feedback_content = f"""ROUTING FEEDBACK:
Request type: {request_type}
Customer request: "{request}"
Correct routing: {correct_tool}
LEARNED RULE: {request_type.upper()} requests (like refunds, billing, payments, charges, invoices) should ALWAYS be routed to {correct_tool}.
This is important institutional knowledge for routing decisions."""
client.retain(
bank_id=bank_id,
content=feedback_content,
context=f"routing:feedback:{request_type}",
metadata={"request_type": request_type, "correct_tool": correct_tool}
)
```
## Phase 1: Without Hindsight (No Memory)
The LLM has no prior knowledge about which channel handles what. With ambiguous tool descriptions, it may route incorrectly.
```python
print("=" * 60)
print("PHASE 1: WITHOUT HINDSIGHT (No Memory)")
print("=" * 60)
phase1_results = []
for i, scenario in enumerate(TEST_SCENARIOS[:3], 1):
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
print(f"Request: \"{scenario['request'][:60]}...\"")
tool_name = make_routing_request(scenario['request'], use_hindsight=False)
is_correct = tool_name == scenario['correct_tool']
phase1_results.append(is_correct)
print(f"LLM chose: {tool_name}")
print(f"Correct tool: {scenario['correct_tool']}")
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
phase1_accuracy = sum(phase1_results) / len(phase1_results) * 100
print(f"\n>>> Phase 1 Accuracy: {phase1_accuracy:.0f}% ({sum(phase1_results)}/{len(phase1_results)})")
```
## Phase 2: Teaching Phase
Now we provide feedback about correct routing to build memory. This simulates a human supervisor correcting the AI's routing decisions.
```python
bank_id = f"tool-learning-{uuid.uuid4().hex[:8]}"
print(f"Using bank_id: {bank_id}")
# Configure and enable Hindsight
hindsight_litellm.configure(
hindsight_api_url=HINDSIGHT_API_URL,
bank_id=bank_id,
store_conversations=True,
inject_memories=True,
max_memories=10,
recall_budget="high",
verbose=False,
)
hindsight_litellm.enable()
print("\nStoring routing feedback...")
feedback_examples = [
("I need a refund for an incorrect charge on my account.", "route_to_channel_alpha", "financial"),
("There's a bug in the system causing data loss.", "route_to_channel_omega", "technical"),
("My billing statement has errors that need correction.", "route_to_channel_alpha", "financial"),
("I want to request a new feature for the dashboard.", "route_to_channel_omega", "technical"),
]
for request, correct_tool, req_type in feedback_examples:
print(f" Storing: {req_type.upper()}{correct_tool}")
store_feedback(bank_id, request, correct_tool, req_type)
print("\nWaiting 15 seconds for Hindsight to process memories...")
time.sleep(15)
print("Done!")
```
## Phase 3: With Hindsight (Memory-Augmented)
The LLM now has access to learned routing knowledge via Hindsight. It should route requests correctly based on past feedback.
```python
print("=" * 60)
print("PHASE 3: WITH HINDSIGHT (Memory-Augmented)")
print("=" * 60)
phase3_results = []
for i, scenario in enumerate(TEST_SCENARIOS, 1):
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
print(f"Request: \"{scenario['request'][:60]}...\"")
tool_name = make_routing_request(
scenario['request'],
use_hindsight=True,
bank_id=bank_id
)
is_correct = tool_name == scenario['correct_tool']
phase3_results.append(is_correct)
print(f"LLM chose: {tool_name}")
print(f"Correct tool: {scenario['correct_tool']}")
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
phase3_accuracy = sum(phase3_results) / len(phase3_results) * 100
print(f"\n>>> Phase 3 Accuracy: {phase3_accuracy:.0f}% ({sum(phase3_results)}/{len(phase3_results)})")
```
## Summary
```python
print("=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"\nPhase 1 (No Memory): {phase1_accuracy:.0f}% accuracy")
print(f"Phase 3 (With Hindsight): {phase3_accuracy:.0f}% accuracy")
improvement = phase3_accuracy - phase1_accuracy
if improvement > 0:
print(f"\n🎉 Improvement: +{improvement:.0f}% accuracy with Hindsight!")
elif improvement == 0:
print(f"\nNote: Results may vary. Run again to see learning effect.")
else:
print(f"\nNote: Phase 1 got lucky! Run again to see typical behavior.")
print(f"\nMemories stored in bank: {bank_id}")
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
print("\n" + "=" * 60)
print("KEY INSIGHT")
print("=" * 60)
print("Hindsight allows the LLM to learn from experience which tool")
print("to use, even when tool names/descriptions are ambiguous.")
```
## Cleanup
```python
hindsight_litellm.cleanup()
# Optional: delete the bank
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted bank: {response.json()}")
```
@@ -1,120 +0,0 @@
---
sidebar_position: 1
---
# Chat Memory App
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/chat-memory)
:::
A demo chat application that uses Groq's `qwen/qwen3-32b` model with Hindsight for persistent per-user memory.
## Features
- 🧠 **Persistent Memory**: Each user gets their own memory bank that remembers conversations
- 🚀 **Fast AI**: Powered by Groq's high-speed inference
- 🎯 **Per-User Context**: Isolated memory per user with automatic context retrieval
- 💬 **Real-time Chat**: Instant responses with memory-augmented context
## Setup
### 1. Start Hindsight API
First, start the Hindsight API server using Docker:
```bash
export GROQ_API_KEY=your_groq_api_key_here
# Start Hindsight with Groq as the LLM provider
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_PROVIDER=groq \
-e HINDSIGHT_API_LLM_API_KEY=$GROQ_API_KEY \
-e HINDSIGHT_API_LLM_MODEL="openai/gpt-oss-20b" \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- **API**: http://localhost:8888
- **Control Plane UI**: http://localhost:9999
### 2. Configure Environment
Copy your Groq API key to the environment file:
```bash
# Update .env.local with your Groq API key
echo "GROQ_API_KEY=your_groq_api_key_here" > .env.local
echo "HINDSIGHT_API_URL=http://localhost:8888" >> .env.local
```
If you don't have one, you can get a free Groq API key here: https://console.groq.com/home
### 3. Install Dependencies
```bash
npm install
```
### 4. Run the App
```bash
npm run dev
```
Open http://localhost:3000 in your browser.
## How It Works
1. **User Identity**: Each browser session gets a unique user ID
2. **Memory Bank Creation**: First message creates a personal memory bank in Hindsight
3. **Context Retrieval**: Before responding, relevant memories are retrieved
4. **Memory Augmented Response**: Groq generates responses with memory context
5. **Conversation Storage**: Each conversation is stored for future context
## Architecture
```
User Message
Next.js API Route (/api/chat)
Hindsight.recall() → Get relevant memories
Groq API → Generate response with memory context
Hindsight.retain() → Store conversation
Response to User
```
## Memory Bank Structure
Each user gets their own isolated memory bank with:
- **Name**: "Chat Memory for [userId]"
- **Background**: Conversational AI assistant context
- **Disposition**: Empathetic (4), Low Skepticism (2), Balanced Literalism (3)
## Try It Out
1. **First Conversation**: Tell the assistant about yourself
- "Hi! I'm a software engineer from San Francisco. I love Python and machine learning."
2. **Second Conversation**: Ask what it remembers
- "What do you know about me?"
- "What programming languages do I like?"
3. **Context Building**: Continue sharing preferences
- "I prefer VS Code over other editors"
- "I'm working on a React project"
4. **Memory Verification**: Visit the Hindsight Control Plane at http://localhost:9999 to see stored memories
## Development
- **Groq Model**: Uses `qwen/qwen3-32b` for fast, high-quality responses
- **Memory Storage**: Automatic conversation retention with context categorization
- **Memory Retrieval**: Semantic search with 2048 token budget for relevant context
@@ -1,145 +0,0 @@
---
sidebar_position: 2
---
# Deliveryman Demo
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/deliveryman-demo)
:::
A delivery agent simulation that demonstrates Hindsight's long-term memory capabilities. An AI agent navigates a multi-building office complex to deliver packages, learning employee locations and optimal paths over time through mental models.
## Prerequisites
- Python 3.11+
- Node.js 18+
- [uv](https://docs.astral.sh/uv/) (Python package manager)
## Setup (Fresh Environment)
### 1. Clone Repositories
```bash
# Clone Hindsight (memory engine)
git clone https://github.com/anthropics/hindsight.git
# Clone the cookbook (contains this demo)
git clone https://github.com/anthropics/hindsight-cookbook.git
```
### 2. Start Hindsight API
```bash
cd hindsight
cp .env.example .env
```
Edit `.env` with your LLM configuration:
```bash
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=<your-groq-api-key>
HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-120b
HINDSIGHT_API_HOST=0.0.0.0
HINDSIGHT_API_PORT=8888
HINDSIGHT_API_ENABLE_OBSERVATIONS=true
# Retain extraction settings (improves employee/location extraction)
HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS="Delivery agent. Remember employee locations, building layout, and optimal paths."
# Embedded database storage
PG0_DATA_DIR=/tmp/hindsight-data
```
Start the API:
```bash
./scripts/dev/start-api.sh
# Runs on http://localhost:8888
```
### 3. Start Hindsight Control Plane (Optional)
The control plane provides a web UI for inspecting memory banks, facts, and mental models.
```bash
cd hindsight
./scripts/dev/start-control-plane.sh
# Runs on a dynamic port (check terminal output)
```
### 4. Start Demo Backend
```bash
cd hindsight-cookbook/deliveryman-demo/backend
# Create virtual environment and install dependencies
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
Create `backend/.env`:
```bash
OPENAI_API_KEY=<your-openai-api-key>
GROQ_API_KEY=<your-groq-api-key>
HINDSIGHT_API_URL=http://localhost:8888
LLM_MODEL=openai/gpt-4o
```
Start the backend:
```bash
./run.sh
# Or manually:
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --ws wsproto --reload
```
**Note:** The `--ws wsproto` flag is required for WebSocket support. Without it, connections will fail with error 1006.
### 5. Start Demo Frontend
```bash
cd hindsight-cookbook/deliveryman-demo/frontend
npm install
npm run dev
# Runs on http://localhost:5173
```
### 6. Open the Demo
Navigate to http://localhost:5173 in your browser.
## How It Works
1. The agent receives a delivery task (e.g., "Deliver Package #3954 to Victor Huang")
2. It navigates a multi-building complex with floors, elevators, and sky bridges
3. Along the way it encounters employees and learns their locations
4. After each delivery, the conversation is sent to Hindsight via the **retain** API
5. Hindsight extracts facts (employee locations, building layout) and builds **mental models**
6. On subsequent deliveries, the agent queries Hindsight to recall what it learned
## Architecture
```
Browser (5173) → Frontend (React + Phaser)
↓ WebSocket
Backend (8000) → FastAPI + Delivery Agent
↓ HTTP
Hindsight API (8888) → Memory Engine + PostgreSQL
```
## Troubleshooting
| Problem | Solution |
|---------|----------|
| WebSocket error 1006 | Restart backend with `--ws wsproto` flag |
| Mental models missing employees | Check `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom` is set |
| Hindsight connection refused | Verify Hindsight API is running on port 8888 |
| Frontend shows "Disconnected" | Check backend is running on port 8000 |
@@ -1,102 +0,0 @@
---
sidebar_position: 3
---
# Go Memory-Augmented API
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/go-memory-service)
:::
A Go HTTP microservice demonstrating per-user memory isolation with Hindsight. Remembers each user's tech stack, problems solved, and preferences to provide personalized assistance.
## Features
- 🔐 **Per-User Isolation**: Each user gets their own memory bank
- 🧠 **Context-Aware Responses**: Uses recall + reflect for personalized answers
- 🏃 **Fire-and-Forget Memory**: Background goroutines store interactions without blocking responses
- 🏷️ **Tag-Based Partitioning**: Organize memories by type (projects, debugging, preferences)
## Setup
### 1. Start Hindsight
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
### 2. Run the service
```bash
go run main.go
```
### 3. Try it out
```bash
# Store memories
curl -s localhost:8080/learn -d '{
"user_id": "alice",
"content": "I am building a Go microservice with gRPC and PostgreSQL",
"tags": ["project"]
}'
curl -s localhost:8080/learn -d '{
"user_id": "alice",
"content": "I prefer structured logging with slog over zerolog",
"tags": ["preferences"]
}'
# Ask questions (uses recall + reflect)
curl -s localhost:8080/ask -d '{
"user_id": "alice",
"query": "What tech stack am I using?"
}' | jq .
# Raw memory recall
curl -s "localhost:8080/recall/alice?q=database" | jq .
```
## API Endpoints
- `POST /learn` - Store new information for a user
- `POST /ask` - Ask a question using the user's memories
- `GET /recall/{userID}?q=query` - Direct memory recall
- `GET /health` - Health check
## Key Patterns
**Per-User Banks**: Each user gets an isolated memory bank (`user-alice`, `user-bob`)
**Async Memory Storage**: Interactions are stored in background goroutines:
```go
go func() {
bgCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
retainReq := hindsight.RetainRequest{
Items: []hindsight.MemoryItem{{
Content: interaction,
Context: *hindsight.NewNullableString(hindsight.PtrString("Q&A interaction")),
}},
}
client.MemoryAPI.RetainMemories(bgCtx, bankID).RetainRequest(retainReq).Execute()
}()
```
**Tag-Based Filtering**: Partition memories within a bank by type for scoped retrieval
## Learn More
- [Go SDK Documentation](https://hindsight.vectorize.io/sdks/go)
- [Hindsight Documentation](https://hindsight.vectorize.io)
@@ -1,206 +0,0 @@
---
sidebar_position: 4
---
# Memory Approaches Comparison Demo
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/hindsight-litellm-demo)
:::
Interactive Streamlit app comparing three memory approaches for LLM applications:
1. **No Memory** - Each query is independent (baseline)
2. **Full Conversation History** - Pass entire conversation (truncated to simulate context limits)
3. **Hindsight Memory** - Intelligent semantic memory retrieval
This demo showcases how Hindsight's semantic memory outperforms traditional approaches, especially as conversations grow longer.
## Quick Start
```bash
# 1. Set your OpenAI API key
export OPENAI_API_KEY=your-key
# 2. Start Hindsight server
docker run -d -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_PROVIDER=openai \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
ghcr.io/vectorize-io/hindsight:latest
# 3. Run the demo
./run.sh
```
Then open http://localhost:8501 in your browser.
## What This Demo Shows
### The Problem with Traditional Approaches
| Approach | How it Works | Limitation |
|----------|--------------|------------|
| **No Memory** | Each query standalone | Forgets everything between messages |
| **Full History** | Pass all messages to LLM | Token limits cause truncation - loses early context |
| **Hindsight** | Semantic retrieval of relevant facts | Retrieves what's relevant regardless of when it was said |
### Key Insight
After 5-10 messages, watch the **Full Conversation History** column start losing early context due to truncation (artificially set to 4 messages to demonstrate this quickly). Meanwhile, **Hindsight Memory** can still recall facts from the beginning because it uses semantic retrieval rather than sequential history.
## Testing the Demo
1. **Introduce yourself**:
- "Hi, I'm Sarah, a data scientist at Netflix"
- "I prefer Python and love machine learning"
2. **Have several exchanges** about different topics
3. **Test recall**:
- "What programming language should I use?"
- "What do you know about me?"
Watch how the three columns respond differently as the conversation grows.
## Features
- **Side-by-side comparison** of all three approaches
- **Debug panels** showing what context each approach uses
- **Memory explorer** to search Hindsight memories directly
- **Configurable settings** for history truncation, max memories, etc.
- **Multi-provider support** via LiteLLM (OpenAI, Anthropic, Groq)
## Prerequisites
- Python 3.10+
- Hindsight server running (Docker recommended)
- At least one LLM API key (OpenAI recommended)
## Setup
### Using run.sh (Recommended)
```bash
# Set API key
export OPENAI_API_KEY=your-key
# Start Hindsight, then run:
./run.sh
```
The script will check and install dependencies automatically.
### Manual Setup
```bash
# Install dependencies
pip install streamlit litellm
# Install Hindsight packages
pip install hindsight-client hindsight-litellm
# Run the app
streamlit run app.py
```
### Starting Hindsight Server
```bash
docker run -d -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_PROVIDER=openai \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
ghcr.io/vectorize-io/hindsight:latest
# Verify it's running
curl http://localhost:8888/health
```
## Configuration
### Sidebar Options
**Model Selection:**
- Provider: OpenAI, Anthropic, Groq
- Model: Various models per provider
- Custom model ID support
**Full History Config:**
- Max Messages to Keep (default: 4 to demonstrate truncation)
**Hindsight Config:**
- API URL (default: http://localhost:8888)
- Bank ID and Entity ID for memory isolation
- Max Memories to retrieve
- Recall Budget (low/mid/high)
**Generation Settings:**
- Temperature
- Max Tokens
- System Prompt
## Supported Models
### OpenAI
- gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo
### Anthropic
- claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022
- claude-3-opus-20240229, claude-3-sonnet-20240229
### Groq
- groq/llama-3.1-70b-versatile, groq/llama-3.1-8b-instant
- groq/mixtral-8x7b-32768
## Environment Variables
```bash
# Required
export OPENAI_API_KEY=sk-...
# Optional (for other providers)
export ANTHROPIC_API_KEY=sk-ant-...
export GROQ_API_KEY=gsk_...
# Optional
export HINDSIGHT_URL=http://localhost:8888
```
## Troubleshooting
### Hindsight server not responding
```bash
# Check if running
curl http://localhost:8888/health
# Start with Docker
docker run -d -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_PROVIDER=openai \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
ghcr.io/vectorize-io/hindsight:latest
```
### hindsight-litellm not installed
```bash
pip install hindsight-litellm
```
### API key errors
Make sure the appropriate API key is set:
```bash
export OPENAI_API_KEY=your-key
```
## Related
- [Hindsight](https://github.com/vectorize-io/hindsight) - Memory infrastructure for AI applications
- [hindsight-litellm](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/litellm) - LiteLLM integration package
## License
MIT
@@ -1,123 +0,0 @@
---
sidebar_position: 5
---
# Tool Learning Demo
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/hindsight-tool-learning-demo)
:::
An interactive Streamlit demo showing how Hindsight helps LLMs learn which tool to use when tool names are ambiguous.
## The Problem
When building AI agents with tool/function calling, tool names and descriptions aren't always clear. An LLM might randomly select between similarly-named tools, leading to incorrect behavior.
## The Scenario
This demo simulates a **customer service routing system** with two channels:
| Tool | Description (What the LLM sees) | Actual Purpose (Hidden) |
|------|--------------------------------|------------------------|
| `route_to_channel_alpha` | "Routes to channel Alpha for appropriate request types" | Financial issues (refunds, billing, payments) |
| `route_to_channel_omega` | "Routes to channel Omega for appropriate request types" | Technical issues (bugs, features, errors) |
The descriptions are **intentionally vague**! Without prior knowledge, the LLM must guess which channel handles what.
## The Solution: Learning with Hindsight
With Hindsight memory:
1. **Store routing feedback** about which channel handles which request type
2. **Retrieve learned knowledge** when making routing decisions
3. **Consistently route correctly** based on past experience
## Quick Start
### Prerequisites
1. **Hindsight Server** running (Docker):
```bash
docker run -d -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_PROVIDER=openai \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
ghcr.io/vectorize-io/hindsight:latest
```
2. **OpenAI API Key**:
```bash
export OPENAI_API_KEY=your-key-here
```
### Run the Demo
```bash
./run.sh
```
Or manually:
```bash
pip install -r requirements.txt
streamlit run app.py
```
## How to Use the Demo
### Step 1: Test Without Memory (Baseline)
1. Select a **Financial Request** (e.g., "I need a refund...")
2. Click **Route Request**
3. Observe: The "Without Hindsight" column may route incorrectly
### Step 2: Route First Customer and Learn
1. Route a customer → Both LLMs route simultaneously
2. Feedback is automatically stored to Hindsight
3. Wait ~5 seconds for Hindsight to index the memory
### Step 3: Test With Memory
1. Select another request (financial or technical)
2. Click **Route Request**
3. Observe: The "With Hindsight" column should now route correctly!
### Step 4: View Statistics
- See accuracy comparison between "Without Memory" vs "With Hindsight"
- Review test history to see the improvement over time
## Demo Features
- **Side-by-side comparison**: See routing results with and without memory
- **Pre-defined test requests**: Financial and technical scenarios
- **Custom requests**: Enter your own customer requests
- **Memory Explorer**: Query stored routing knowledge directly
- **Live statistics**: Track accuracy improvement
## Key Insight
> Even when tool names and descriptions don't reveal their purpose, Hindsight allows the LLM to **learn from experience** which tool to use for which type of request.
This is especially valuable for:
- Enterprise systems with legacy tool names
- Multi-tenant systems where tools have generic names
- Agents that need to learn organization-specific workflows
## Configuration
| Setting | Default | Description |
|---------|---------|-------------|
| Model | gpt-4o-mini | LLM model for routing decisions |
| Temperature (No Memory) | 0.7 | Randomness for baseline tests |
| Hindsight API URL | http://localhost:8888 | Hindsight server URL |
## Files
- `app.py` - Main Streamlit application
- `requirements.txt` - Python dependencies
- `run.sh` - Launch script with dependency checking
- `README.md` - This file
@@ -1,315 +0,0 @@
---
sidebar_position: 6
---
# OpenAI Agent + Hindsight Memory Integration
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/openai-fitness-coach)
:::
A fitness coach example demonstrating how to use **OpenAI Agents** with **Hindsight as a memory backend**.
## What This Demonstrates
This example showcases:
- **OpenAI Assistants** handling conversation logic
- **Hindsight** providing sophisticated memory storage & retrieval
- **Function calling** to bridge them together
- **Streaming responses** for real-time interaction (enabled by default)
- **Bidirectional memory** - both user data AND coach observations stored
- **System-level post-processing** - automatic opinion storage for reliability
- **Temporal-semantic memory** queries via function tools
- **Enhanced preference learning** - coach learns and respects user likes/dislikes
- **Real-world integration pattern** for adding memory to AI agents
## Architecture
```
User: "I ran 5K today, don't like tempo runs"
|
OpenAI Assistant
|
Function Call: store_memory(workout + preference)
|
Hindsight API (stores as world/agent)
|
OpenAI Assistant: "What should I focus on?"
|
Function Call: retrieve_memories("workouts and preferences")
|
Hindsight API (returns workouts + preferences)
|
OpenAI Assistant (analyzes, gives advice)
|
Function Call: store_memory(advice as opinion)
|
Hindsight API (stores coach's observation)
|
Personalized Answer
```
## Key Difference from Standard Demo
| Component | Standard Demo | OpenAI Integration |
|-----------|---------------|-------------------|
| **Conversation** | Hindsight `/think` endpoint | OpenAI Assistant API |
| **Memory** | Hindsight (built-in) | Hindsight (via function calling) |
| **LLM** | Configured in Hindsight | OpenAI GPT-4 |
| **Opinion Formation** | Automatic in `/think` | Explicit via `store_memory(type="opinion")` |
| **Best For** | Hindsight-native apps | Integrating memory into existing OpenAI agents |
## Quick Start
### Prerequisites
1. **OpenAI API Key**
```bash
export OPENAI_API_KEY=your_openai_api_key
```
2. **Hindsight API running**
```bash
# Follow Hindsight setup instructions to start the API
# Default: http://localhost:8888
```
3. **Install dependencies**
```bash
pip install openai requests
```
### Run the Conversational Demo
```bash
cd openai-fitness-coach
export OPENAI_API_KEY=your_key_here
python demo_conversational.py
```
The demo showcases:
1. **Natural language workout logging** - Tell the coach what you did conversationally
2. **Preference learning** - Express likes/dislikes and watch the coach adapt
3. **Goal tracking** - Set goals, track progress, achieve milestones
4. **Bidirectional memory** - Both your activities AND coach's advice are stored
5. **Streaming responses** - See responses appear in real-time
6. **7 interactive phases** - From goal setting to achievement recognition
The demo uses a separate agent (`fitness-coach-demo`) to avoid mixing with real data.
## Usage
### Chat with Your Coach
**Interactive mode:**
```bash
python openai_coach.py
```
**Single question:**
```bash
python openai_coach.py "What did I do for training this week?"
```
## How It Works
### 1. Memory Tools (`memory_tools.py`)
Defines function tools that the OpenAI Agent can call:
```python
retrieve_memories(query, fact_types, top_k)
search_workouts(after_date, before_date, workout_type)
get_nutrition_summary(after_date, before_date)
get_user_goals()
get_coach_opinions(about)
```
Each function makes API calls to Hindsight to fetch relevant memories.
### 2. OpenAI Agent (`openai_coach.py`)
Creates an OpenAI Assistant with:
- Fitness coaching instructions
- Access to memory function tools
- Conversation management
When you ask a question:
1. User message is sent to OpenAI Assistant
2. Assistant decides which memory functions to call
3. Functions fetch data from Hindsight
4. Assistant generates response using retrieved context
### 3. Function Calling Flow
```python
# User asks: "What did I run this week?"
# OpenAI Assistant decides to call:
search_workouts(
after_date="2024-11-18",
workout_type="running"
)
# Function retrieves from Hindsight:
{
"results": [
{"text": "User completed 45-minute cardio workout: running..."},
{"text": "User completed 60-minute cardio workout: running..."}
]
}
# OpenAI Assistant generates response:
"This week you've done two runs: a 45-minute run on Monday
and a longer 60-minute run on Wednesday. Great consistency!"
```
## Example Questions
Try asking:
```bash
python openai_coach.py "What does my training look like this week?"
python openai_coach.py "Based on my workouts, should I rest today?"
python openai_coach.py "How is my nutrition supporting my goals?"
python openai_coach.py "What's my progress toward my goal?"
python openai_coach.py "Compare my training this month to last month"
```
The agent will automatically:
1. Identify what memories it needs
2. Call the appropriate function tools
3. Retrieve data from Hindsight
4. Generate a personalized response
## Memory Types Retrieved
The OpenAI Agent can retrieve different memory types from Hindsight:
- **World Facts** (`fact_type: "world"`): Workouts, meals, activities
- **Agent Facts** (`fact_type: "agent"`): Goals, intentions
- **Opinions** (`fact_type: "opinion"`): Coach's observations about patterns
## Customization
### Add New Function Tools
Edit `memory_tools.py` to add new capabilities:
```python
def get_weekly_summary(week_offset: int = 0):
"""Get a summary of a specific week."""
# Implementation
pass
# Add to MEMORY_TOOLS list
MEMORY_TOOLS.append({
"type": "function",
"function": {
"name": "get_weekly_summary",
"description": "Get training summary for a specific week",
# ... parameters
}
})
# Add to FUNCTION_MAP
FUNCTION_MAP["get_weekly_summary"] = get_weekly_summary
```
### Modify Assistant Instructions
Edit `openai_coach.py` to change the coach's personality or behavior:
```python
assistant = client.beta.assistants.create(
name="Your Custom Coach",
instructions="Your custom instructions here...",
model="gpt-4o-mini",
tools=MEMORY_TOOLS
)
```
## Use Cases
This pattern works for any application that needs memory:
1. **Customer Support Agents** - Remember past conversations and issues
2. **Personal Assistants** - Remember preferences, schedules, past decisions
3. **Educational Tutors** - Track learning progress over time
4. **Health Coaches** - Monitor habits, progress, goals (like this example)
5. **Sales Assistants** - Remember customer interactions and preferences
## Integration Pattern
**To add Hindsight memory to your own OpenAI Agent:**
1. Define function tools that call Hindsight API
2. Register them with your OpenAI Assistant
3. Implement function handlers to execute Hindsight queries
4. Let OpenAI Assistant decide when to retrieve memories
The key benefit: **Separation of concerns**
- OpenAI = Conversation logic
- Hindsight = Memory storage, retrieval, temporal queries, entity linking
## When to Use This vs. Standard Hindsight
**Use OpenAI + Hindsight (this example) when:**
- You want OpenAI's conversation capabilities
- You're already using OpenAI Agents
- You want explicit control over when to retrieve memories
- You want to combine Hindsight with other OpenAI features
**Use Hindsight directly when:**
- You want a complete memory-first solution
- You want automatic memory retrieval and opinion formation
- You want to use different LLM providers (not just OpenAI)
- You want the `/think` endpoint's integrated approach
## Learning Points
After running this demo, you'll understand:
1. How to add sophisticated memory to any OpenAI Agent
2. How function calling bridges LLMs and memory systems
3. How temporal-semantic queries work via function tools
4. Real-world pattern for LLM + memory integration
## Core Files
- `demo_conversational.py` - Conversational demo showcasing preference learning and goal tracking
- `openai_coach.py` - OpenAI Assistant wrapper with streaming and memory integration
- `memory_tools.py` - Function calling tools that bridge to Hindsight API
- `.openai_assistant_id` - Saved assistant ID (auto-generated, gitignored)
## Common Issues
**"OPENAI_API_KEY not set"**
```bash
export OPENAI_API_KEY=your_api_key_here
```
**"Agent not found"**
- Make sure the Hindsight fitness-coach agent exists
**"Connection refused"**
- Make sure Hindsight API is running on localhost:8888
## Next Steps
1. Run the demo to see it in action
2. Try chatting with the coach: `python openai_coach.py`
3. Log your own workouts and meals
4. Experiment with different questions
5. Add custom function tools for your use case
---
**Built with:**
- OpenAI Assistants API
- Hindsight (temporal-semantic memory)
- Function calling for integration
@@ -1,371 +0,0 @@
---
sidebar_position: 7
---
# Sanity CMS Blog Memory
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/sanity-blog-memory)
:::
A Hindsight cookbook recipe demonstrating how to sync blog posts from **Sanity CMS** to Hindsight agent memory, enabling semantic search, temporal queries, and AI-powered content insights.
## Features
- **Blog Post Sync**: Automatically sync all blog posts from Sanity to Hindsight
- **Document-based Upsert**: Idempotent syncing with `document_id` - re-running sync updates existing content
- **Semantic Search**: Find related content using natural language queries
- **Temporal Queries**: Ask "What did I write in January 2025?"
- **Reflect for Insights**: Generate AI-powered analysis of your blog content
- **Related Content Discovery**: Power "Related Posts" features with semantic similarity
## Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Sanity CMS │───────▶│ Sync Script │───────▶│ Hindsight │
│ (Content) │ GROQ │ (TypeScript) │ HTTP │ (Memory) │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ │
│ Your App │
│ - Recall │
│ - Reflect │
│ │
└─────────────────┘
```
## Quick Start
### 1. Start Hindsight
Choose your preferred LLM provider:
**Option A: Using Docker Compose (Recommended)**
```bash
# Set your API key
export OPENAI_API_KEY=sk-...
# OR
export GOOGLE_API_KEY=... # Gemini (free tier available)
# OR
export GROQ_API_KEY=... # Groq (free tier available)
# Start Hindsight
docker compose up -d
```
**Option B: Using Docker directly**
```bash
export OPENAI_API_KEY=sk-...
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- **API**: http://localhost:8888
- **Control Plane UI**: http://localhost:9999
### 2. Configure Environment
```bash
# Copy example config
cp .env.example .env
# Edit with your values
nano .env
```
Required settings:
```bash
# Hindsight
HINDSIGHT_API_URL=http://localhost:8888
HINDSIGHT_BANK_ID=blog-memory
# Sanity CMS
SANITY_PROJECT_ID=your-project-id
SANITY_DATASET=production
```
### 3. Install Dependencies
```bash
npm install
```
### 4. Sync Your Blog Posts
```bash
npm run sync
```
Expected output:
```
=======================================
Sanity -> Hindsight Blog Sync
=======================================
Setting up memory bank...
Memory bank "blog-memory" ready
Fetching posts from Sanity CMS...
Found 10 posts to sync
Syncing posts to Hindsight...
[1/10] "Why I Chose Qwik"... done
[2/10] "Building AI Agents"... done
...
=======================================
Sync Complete
=======================================
Synced: 10 posts
```
### 5. Query Your Content
```bash
npm run query
```
## Query Examples
### Semantic Search
Find related content using natural language:
```typescript
import { recallMemory } from './hindsight-client.js';
// Find posts about AI agents
const result = await recallMemory('AI agents and automation', {
budget: 'mid',
maxTokens: 2048,
});
console.log(`Found ${result.results.length} relevant posts`);
```
### Temporal Queries
Ask about content from specific time periods:
```typescript
// Posts from January 2025
const result = await recallMemory('What did I write about in January 2025?', {
queryTimestamp: '2025-01-31T23:59:59Z',
});
```
### Reflect for Insights
Generate AI-powered analysis of your content:
```typescript
import { reflectOnMemory } from './hindsight-client.js';
// Analyze blog themes
const insights = await reflectOnMemory(
'What are the main themes of my blog? What topics do I write about most?',
{ budget: 'high' }
);
console.log(insights.text);
```
### Related Content Discovery
Power your "Related Posts" feature:
```typescript
// Find posts similar to a specific article
const related = await recallMemory(
'Find posts related to "Why I Chose Qwik for My Personal Website"',
{ budget: 'mid' }
);
```
## Memory Structure
Each blog post is stored with rich metadata for optimal recall:
```
# Blog Post: {title}
**Published:** {date}
**URL:** {base_url}/blog/{slug}
**Tags:** {tags}
**Reading Time:** {reading_time}
## Description
{description}
## Content
{full_content}
```
Key features:
- **document_id**: `post:{slug}` - Enables upsert on re-sync
- **context**: `blog-post` - Categorizes the memory type
- **timestamp**: Post publication date - Enables temporal queries
## Use Cases
### 1. AI-Powered Blog Search
Replace keyword search with semantic understanding:
```typescript
// Old: keyword matching
const results = posts.filter(p => p.title.includes('React'));
// New: semantic understanding
const result = await recallMemory('frontend framework tutorials');
```
### 2. Content Recommendation Engine
Generate personalized recommendations:
```typescript
const recommendations = await reflectOnMemory(
'Based on a reader interested in "AI automation", recommend related posts'
);
```
### 3. Writing Assistant
Get topic suggestions based on your existing content:
```typescript
const suggestions = await reflectOnMemory(
'What topics should I write about next? What gaps exist in my content?'
);
```
### 4. Content Analytics
Analyze your blog's evolution:
```typescript
const analysis = await reflectOnMemory(
'How have my writing topics evolved over the past year?'
);
```
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_URL` | Hindsight API endpoint | `http://localhost:8888` |
| `HINDSIGHT_BANK_ID` | Memory bank identifier | `blog-memory` |
| `SANITY_PROJECT_ID` | Your Sanity project ID | (required) |
| `SANITY_DATASET` | Sanity dataset name | `production` |
| `SANITY_API_TOKEN` | Sanity API token (for private datasets) | (none) |
| `SANITY_API_VERSION` | Sanity API version | `2024-01-09` |
| `SITE_URL` | Your blog's base URL | `https://example.com` |
### Memory Bank Disposition
The memory bank is configured with disposition traits optimized for blog content:
```typescript
{
skepticism: 2, // Trusting - blog content is authoritative
literalism: 4, // Literal - exact content matters
empathy: 3, // Balanced
}
```
## Extending for Other CMS Platforms
This pattern can be adapted for any CMS. The key components:
### 1. CMS Client
Replace `sanity-client.ts` with your CMS:
```typescript
// contentful-client.ts
import { createClient } from 'contentful';
export async function getAllPosts(): Promise<BlogPost[]> {
const client = createClient({...});
const entries = await client.getEntries({ content_type: 'blogPost' });
return entries.items.map(transformPost);
}
```
### 2. Content Transformation
Ensure your content is formatted for semantic search:
```typescript
function formatPostContent(post: BlogPost): string {
return `# ${post.title}
**Published:** ${post.date}
...
${post.content}`;
}
```
### 3. Document ID Strategy
Use a consistent document ID for upsert behavior:
```typescript
await retainBlogPost(content, {
documentId: `post:${post.slug}`, // Unique, stable identifier
timestamp: post.date,
});
```
## Troubleshooting
### "Connection refused" error
Make sure Hindsight is running:
```bash
docker compose up -d
curl http://localhost:8888/health
```
### "No posts found" during sync
Check your Sanity configuration:
```bash
# Verify project ID
echo $SANITY_PROJECT_ID
# Test GROQ query
npx sanity query '*[_type == "post"][0..2]{title}'
```
### Slow recall/reflect responses
This is normal for the first query as Hindsight builds embeddings. Subsequent queries are faster. Use `budget: 'low'` for faster responses at the cost of recall quality.
## Resources
- [Hindsight Documentation](https://hindsight.vectorize.io/)
- [Hindsight GitHub](https://github.com/vectorize-io/hindsight)
- [Sanity CMS Documentation](https://www.sanity.io/docs)
- [Hindsight Cookbook](https://github.com/vectorize-io/hindsight-cookbook)
## License
MIT
@@ -1,276 +0,0 @@
---
sidebar_position: 8
---
# Stance Tracker
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/stancetracker)
:::
An AI-powered application that tracks political candidates' stances on issues over time using Hindsight memory system and web scraping.
## Features
- **Geographic Targeting**: Track stances by country, state/province, and city
- **Multi-Candidate Tracking**: Monitor multiple candidates simultaneously
- **Temporal Analysis**: Historical stance tracking with configurable time ranges
- **Automated Scraping**: Periodic content collection with configurable frequencies (hourly/daily/weekly)
- **Stance Change Detection**: Automatic detection and highlighting of position changes
- **Interactive Timeline**: Visual graph showing stance evolution with reference callouts
- **Source Attribution**: All stances linked to verified sources with excerpts
## Architecture
### Memory System (Hindsight Integration)
This app uses the Hindsight memory system from `github.com/vectorize-io/hindsight`:
1. **Banks**: Each scraper agent has its own memory bank
2. **Retain**: Stores candidate statements and web scraping results
3. **Recall**: Semantic search to retrieve relevant memories
4. **Reflect**: Generates contextual analysis using stored memories
5. **Temporal Search**: Queries memories within specific time periods
### Tech Stack
- **Frontend**: Next.js 16, React, TypeScript, TailwindCSS
- **Visualization**: Recharts for timeline graphs
- **Backend**: Next.js API routes
- **Memory**: Hindsight (from github.com/vectorize-io/hindsight)
- **Database**: JSON file storage (no database required)
- **Web Search**: Tavily API
- **LLM**: OpenAI/Anthropic/Groq (configurable)
- **Scheduling**: node-cron
## Prerequisites
1. **Hindsight API** running (from github.com/vectorize-io/hindsight)
2. **API Keys**:
- Tavily API key (for web search)
- LLM provider API key (OpenAI, Anthropic, or Groq)
## Setup
### 1. Install Dependencies
```bash
npm install
```
### 2. Configure Environment
Copy `.env.example` to `.env` and fill in your credentials:
```bash
cp .env.example .env
```
Edit `.env`:
```env
# Hindsight API (from github.com/vectorize-io/hindsight)
HINDSIGHT_API_URL=http://localhost:8888
# Tavily API (for web search)
TAVILY_API_KEY=your_tavily_api_key_here
# LLM Provider
LLM_PROVIDER=openai # or anthropic, groq
LLM_API_KEY=your_llm_api_key_here
LLM_MODEL=gpt-4-turbo-preview
```
### 3. Start Hindsight
Clone and run Hindsight from github.com/vectorize-io/hindsight:
```bash
# Clone and run github.com/vectorize-io/hindsight
cd /path/to/hindsight
cargo run --bin hindsight-server
```
Verify Hindsight is running at `http://localhost:8888`
### 4. Run the Application
```bash
npm run dev
```
Visit `http://localhost:3000`
## Usage
### Creating a Tracking Session
1. **Set Location**: Enter country (required), state/province, and city (optional)
2. **Choose Topic**: Specify the issue to track (e.g., "Climate Change Policy")
3. **Add Candidates**: Enter names of candidates/politicians to track
4. **Configure Time Range**: Set historical start/end dates for initial analysis
5. **Set Frequency**: Choose how often to check for updates (hourly/daily/weekly)
6. **Start Tracking**: Click "Start Tracking" to begin
### Viewing Results
- **Timeline Graph**: Shows confidence levels of each candidate's stance over time
- **Stance Changes**: Red circles on the graph indicate detected position changes
- **Click Points**: Click any point to see detailed stance information and sources
- **Source Links**: Each stance includes links to original references
### Managing Sessions
- **Pause/Resume**: Temporarily stop or restart tracking
- **Run Now**: Trigger an immediate update outside the schedule
- **Status**: View current session status and frequency
## API Endpoints
### Sessions
- `POST /api/sessions` - Create new tracking session
- `GET /api/sessions?id={id}` - Get session details
- `GET /api/sessions` - List all sessions
- `PATCH /api/sessions` - Update session status
### Stances
- `POST /api/stances` - Process candidate stance
- `GET /api/stances?sessionId={id}&candidate={name}` - Get stances
### Scheduler
- `POST /api/scheduler` - Control session scheduling
- Actions: `start`, `stop`, `run`
## Hindsight Integration Examples
### 1. Storing Memories
```typescript
// Store web scraping results
await hindsightClient.retain(bankId, articleContent, {
context: 'web_search_result',
timestamp: articleDate,
metadata: { url: articleUrl }
});
```
### 2. Semantic Search
```typescript
// Search for relevant memories
const results = await hindsightClient.recall(bankId, query, {
budget: 'high',
maxTokens: 8192
});
```
### 3. Temporal Filtering
```typescript
// Query memories up to a specific point in time
const results = await hindsightClient.recall(bankId, query, {
queryTimestamp: '2024-12-01T00:00:00Z'
});
```
### 4. Contextual Analysis
```typescript
// Generate analysis using stored memories
const response = await hindsightClient.reflect(bankId,
'What is the candidate\'s stance on this issue?',
{ budget: 'high' }
);
```
## Production Deployment
### Vercel Deployment
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Set environment variables in Vercel dashboard:
# - HINDSIGHT_API_URL
# - TAVILY_API_KEY
# - LLM_PROVIDER
# - LLM_API_KEY
# - LLM_MODEL
```
**Note**: The `data/` directory for JSON storage will be ephemeral on Vercel. For production, consider using a persistent database or object storage.
## Development
### Project Structure
```
stancetracker/
├── app/
│ ├── api/ # API routes
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Main page
├── components/ # React components
├── lib/
│ ├── db/ # JSON database utilities
│ ├── hindsight-client.ts # Hindsight API client
│ ├── llm-client.ts # LLM provider client
│ ├── web-scraper.ts # Tavily web scraper
│ ├── scraper-agent.ts # Content scraper
│ ├── rag-system.ts # Memory retrieval
│ ├── stance-extractor.ts # Stance analysis
│ ├── stance-pipeline.ts # Main pipeline
│ └── scheduler.ts # Job scheduling
└── types/ # TypeScript types
```
### Adding New LLM Providers
Edit `lib/llm-client.ts` and add a new method:
```typescript
private async newProviderComplete(messages, options) {
// Implementation
}
```
## Limitations
- **Web Search**: Uses Tavily API which has rate limits
- **Source Verification**: Manual verification recommended for critical applications
- **Stance Extraction**: LLM-based, subject to model limitations
- **Storage**: JSON file storage is not suitable for high-scale production use
- **Rate Limits**: Respect API rate limits for Tavily, Hindsight, and LLM providers
## Future Enhancements
- [ ] Real-time social media monitoring
- [ ] Speech/video transcription analysis
- [ ] Multi-language support
- [ ] Sentiment analysis integration
- [ ] Comparative analysis dashboard
- [ ] Export to CSV/PDF
- [ ] Email notifications for stance changes
- [ ] Public API for third-party integrations
## License
MIT
## Support
For issues or questions, please check:
- Hindsight documentation: `github.com/vectorize-io/hindsight/README.md`
- Tavily API docs: https://tavily.com/
- Project issues: Create an issue in the repository
@@ -1,122 +0,0 @@
---
sidebar_position: 9
---
# Hindsight AI SDK - Personal Chef
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/taste-ai)
:::
A personal food assistant demonstrating three key Hindsight integrations using the [Vercel AI SDK v6](https://sdk.vercel.ai/docs).
## Architecture: Single Bank with User Tags
This demo uses a **single Hindsight bank** (`taste-ai`) for all users, with each user's data tagged using `user:${username}`.
```typescript
// All users share the same bank
const BANK_ID = 'taste-ai';
// Each memory is tagged with the user
await hindsightTools.retain.execute({
bankId: BANK_ID,
content: userData,
tags: [`user:${username}`],
});
```
This architecture enables:
- **Per-user queries**: Filter by `user:alice` to get personalized results
- **Aggregated insights**: Query across all users to find popular recipes or common dietary patterns
- **Simplified management**: One bank to maintain instead of per-user banks
## Three Hindsight Integrations
### 1. Meal Suggestions with Memory Recall & Reflection
Uses `recall` and `reflect` tools with AI SDK's agent-based approach to gather personalized context.
```typescript
const contextResult = await generateText({
model: llmModel,
tools: {
recall: hindsightTools.recall,
reflect: hindsightTools.reflect,
},
toolChoice: 'auto',
prompt: `You are gathering context for personalized ${mealType} recipe suggestions.
Use the recall tool to search for the user's food preferences, dislikes, and recent meals.
Then use the reflect tool to analyze their dietary patterns and restrictions.
After gathering context, summarize their preferences and recent eating patterns.`,
});
```
The AI agent autonomously:
- Searches memory for cuisine preferences and dietary restrictions
- Analyzes recent protein consumption for variety
- Identifies foods to avoid
### 2. Goal Progress Tracking with Mental Models
Uses mental models to automatically maintain updated insights about user progress.
```typescript
// Create a mental model that auto-refreshes after new meals
await hindsightTools.createMentalModel.execute({
bankId: BANK_ID,
mentalModelId: getMentalModelId(username, 'goals'),
name: `${username}'s Goal Progress`,
sourceQuery: `Analyze ${username}'s dietary goals and eating patterns.
Describe their progress towards their stated goals (weight loss, muscle gain, etc.).`,
tags: [`user:${username}`],
autoRefresh: true, // Refreshes automatically after consolidation
});
// Query the mental model for current insights
const result = await hindsightTools.queryMentalModel.execute({
bankId: BANK_ID,
mentalModelId: mentalModelId,
});
```
Mental models automatically:
- Track progress towards dietary goals
- Update after each new meal is logged
- Provide fresh insights without manual refresh
### 3. Language Enforcement with Directives
Uses directives to ensure all responses match user's language preference.
```typescript
await hindsightClient.createDirective(BANK_ID, {
name: `${username}'s Language Preference`,
content: `Always respond in ${language}. All suggestions must be in ${language}.`,
priority: 100,
tags: [`user:${username}`, 'directive:language'],
});
```
Directives are automatically injected when mental models generate insights, ensuring consistent language across all interactions.
## Running the Demo
```bash
npm install
npm run dev
```
**Requirements:**
- Hindsight server running at `http://localhost:8888` (or set `HINDSIGHT_URL`)
- Node.js 18+
## Learn More
- [Hindsight AI SDK on npm](https://www.npmjs.com/package/@vectorize-io/hindsight-ai-sdk)
- [AI SDK Documentation](https://sdk.vercel.ai/docs)
@@ -1,153 +0,0 @@
---
sidebar_position: 1
hide_table_of_contents: true
pagination_next: null
pagination_prev: null
custom_edit_url: null
sidebar_class_name: hidden-sidebar
---
import RecipeCarousel from '@site/src/components/RecipeCarousel';
<div className="cookbook-page">
# Cookbook
Learn how to build with Hindsight through practical examples:
- **Recipes** - Step-by-step guides and patterns for common use cases
- **Applications** - Complete, runnable applications demonstrating Hindsight integration
<RecipeCarousel
title="Recipes"
items={[
{
title: "Hindsight Quickstart",
href: "/cookbook/recipes/quickstart",
description: "Learn the basics: retain, recall, and reflect",
tags: { sdk: "hindsight-client", topic: "Quick Start" }
},
{
title: "Per-User Memory",
href: "/cookbook/recipes/per-user-memory",
description: "Build a chatbot with per-user memory isolation",
tags: { sdk: "hindsight-client", topic: "Learning" }
},
{
title: "Support Agent with Shared Knowledge",
href: "/cookbook/recipes/support-agent-shared-knowledge",
description: "Combine per-user memory with shared product documentation",
tags: { sdk: "hindsight-client", topic: "Learning" }
},
{
title: "Memory with LiteLLM",
href: "/cookbook/recipes/litellm-memory-demo",
description: "Add automatic memory to any LLM app using LiteLLM callbacks",
tags: { sdk: "hindsight-litellm", topic: "Quick Start" }
},
{
title: "Routing Tool Learning",
href: "/cookbook/recipes/tool-learning-demo",
description: "Teach an LLM which tool to use through feedback and memory",
tags: { sdk: "hindsight-litellm", topic: "Learning" }
},
{
title: "Fitness Coach with Hindsight Memory",
href: "/cookbook/recipes/fitness_tracker",
description: "Track workouts, diet, and progress with a personalized fitness coach",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Healthcare Assistant with Hindsight Memory",
href: "/cookbook/recipes/healthcare_assistant",
description: "A supportive chatbot that remembers patient history and preferences",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Movie Recommendation Assistant with Hindsight Memory",
href: "/cookbook/recipes/movie_recommendation",
description: "Get personalized movie recommendations that improve over time",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Personal AI Assistant with Hindsight Memory",
href: "/cookbook/recipes/personal_assistant",
description: "A general-purpose assistant that remembers your life and preferences",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Personalized Search Agent with Hindsight Memory",
href: "/cookbook/recipes/personalized_search",
description: "Search assistant that learns your location, diet, and lifestyle",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Study Buddy with Hindsight Memory",
href: "/cookbook/recipes/study_buddy",
description: "Track study sessions, identify knowledge gaps, and get personalized review suggestions",
tags: { sdk: "hindsight-client", topic: "Learning" }
}
]}
/>
<RecipeCarousel
title="Applications"
items={[
{
title: "Chat Memory App",
href: "/cookbook/applications/chat-memory",
description: "Real-time chat app with per-user memory using Groq and Hindsight",
tags: { sdk: "hindsight-client", topic: "Chat" }
},
{
title: "Deliveryman Demo",
href: "/cookbook/applications/deliveryman-demo",
description: "Delivery agent simulation demonstrating learning through mental models",
tags: { sdk: "hindsight-client", topic: "Learning" }
},
{
title: "Go Memory-Augmented API",
href: "/cookbook/applications/go-memory-service",
description: "Go HTTP microservice with per-user memory banks for a developer knowledge assistant",
tags: { sdk: "hindsight-go", topic: "Learning" }
},
{
title: "Memory Approaches Comparison Demo",
href: "/cookbook/applications/hindsight-litellm-demo",
description: "Interactive comparison of memory approaches: none, full history, and semantic retrieval",
tags: { sdk: "hindsight-litellm", topic: "Quick Start" }
},
{
title: "Tool Learning Demo",
href: "/cookbook/applications/hindsight-tool-learning-demo",
description: "Show how Hindsight helps LLMs learn which tool to use when names are ambiguous",
tags: { sdk: "hindsight-litellm", topic: "Learning" }
},
{
title: "OpenAI Agent + Hindsight Memory Integration",
href: "/cookbook/applications/openai-fitness-coach",
description: "Fitness coach using OpenAI Assistants with Hindsight as memory backend",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Sanity CMS Blog Memory",
href: "/cookbook/applications/sanity-blog-memory",
description: "Sync Sanity CMS blog posts to Hindsight for semantic search and AI insights",
tags: { sdk: "hindsight-client", topic: "Learning" }
},
{
title: "Stance Tracker",
href: "/cookbook/applications/stancetracker",
description: "Track political candidates' stances over time with automated web scraping",
tags: { sdk: "hindsight-client", topic: "Recommendation" }
},
{
title: "Hindsight AI SDK - Personal Chef",
href: "/cookbook/applications/taste-ai",
description: "Personal food assistant with AI SDK v6 showcasing recall, mental models, and directives",
tags: { sdk: "@vectorize-io/hindsight-ai-sdk", topic: "Recommendation" }
}
]}
/>
</div>
@@ -1,306 +0,0 @@
---
sidebar_position: 6
---
# Fitness Coach with Hindsight Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/fitness_tracker.ipynb)
:::
A personalized fitness assistant that tracks your workouts, diet, recovery, and progress over time to give contextual advice.
## Features
- Logs workout sessions with exercises and weights
- Tracks meals and dietary preferences
- Monitors recovery and sleep patterns
- Provides personalized training advice
## Prerequisites
- OpenAI API key
- Hindsight running locally via Docker (see setup below)
## Start Hindsight Locally
Before running this notebook, start Hindsight in a terminal:
```bash
export OPENAI_API_KEY="your-openai-api-key"
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## 1. Install Dependencies
```python
!pip install -q hindsight-client openai nest-asyncio
```
## 2. Configure OpenAI API Key
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
```python
import getpass
import os
# Set OpenAI API key (used by both Hindsight and the demo)
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
print("API key configured!")
```
## 3. Initialize Clients
```python
import nest_asyncio
nest_asyncio.apply()
from datetime import datetime
from openai import OpenAI
from hindsight_client import Hindsight
# Initialize Hindsight client (connects to local Docker instance)
hindsight = Hindsight(
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
)
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
USER_ID = "fitness-user-demo"
print("Clients initialized!")
```
## 4. Define Helper Functions
```python
def log_workout(workout_details: str) -> str:
"""Log a workout session with timestamp."""
today = datetime.now().strftime("%B %d, %Y")
hindsight.retain(
bank_id=USER_ID,
content=f"{today} - WORKOUT LOG: {workout_details}",
metadata={"category": "workout", "date": today},
)
return f"Logged workout for {today}: {workout_details}"
def log_meal(meal_details: str) -> str:
"""Log a meal with timestamp."""
today = datetime.now().strftime("%B %d, %Y")
hindsight.retain(
bank_id=USER_ID,
content=f"{today} - MEAL LOG: {meal_details}",
metadata={"category": "nutrition", "date": today},
)
return f"Logged meal for {today}: {meal_details}"
def log_recovery(recovery_details: str) -> str:
"""Log recovery information (sleep, soreness, etc.)."""
today = datetime.now().strftime("%B %d, %Y")
hindsight.retain(
bank_id=USER_ID,
content=f"{today} - RECOVERY LOG: {recovery_details}",
metadata={"category": "recovery", "date": today},
)
return f"Logged recovery for {today}: {recovery_details}"
def store_user_profile(profile_info: str) -> str:
"""Store user profile information."""
hindsight.retain(
bank_id=USER_ID,
content=f"USER PROFILE: {profile_info}",
metadata={"category": "profile"},
)
return f"Stored profile info: {profile_info}"
def fitness_coach(user_query: str) -> str:
"""Get personalized fitness advice based on query and user history."""
memories = hindsight.recall(
bank_id=USER_ID,
query=f"fitness workout diet recovery goals {user_query}",
budget="high",
)
memory_context = ""
if memories and memories.results:
memory_context = "\n".join(f"- {m.text}" for m in memories.results[:10])
system_prompt = f"""You are a knowledgeable and supportive fitness coach.
You have access to the user's workout history, diet logs, recovery notes, and personal profile.
What you know about this user:
{memory_context if memory_context else "No history recorded yet."}
Provide personalized, actionable advice based on their:
- Training history and progress
- Dietary preferences and restrictions
- Recovery patterns
- Personal goals
Be encouraging but realistic. Reference their specific history when relevant."""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query},
],
temperature=0.7,
max_tokens=600,
)
advice = response.choices[0].message.content
hindsight.retain(
bank_id=USER_ID,
content=f"User asked: {user_query}\nCoach advised: {advice[:200]}...",
metadata={"category": "coaching"},
)
return advice
def get_progress_report() -> str:
"""Generate a progress report based on workout history."""
report = hindsight.reflect(
bank_id=USER_ID,
query="""Analyze this user's fitness journey:
1. How consistent have they been with workouts?
2. What progress have they made (weight lifted, exercises)?
3. How is their recovery and sleep?
4. What dietary patterns do you notice?
5. What should they focus on next?""",
budget="high",
)
return report.text if hasattr(report, 'text') else str(report)
print("Helper functions defined!")
```
## 5. Set Up User Profile
```python
print("Setting up user profile...")
profile_data = [
"Name: Anish, Age: 26, Height: 5'10\", Weight: 72kg",
"Goal: Building lean muscle, started gym 6 months ago",
"Routine: Push-pull-legs split, 5x per week",
"Rest days: Wednesday and Sunday",
"Dietary restriction: Mild lactose intolerance, uses almond milk",
"Health note: Occasional knee pain, avoids deep squats",
"Supplements: Whey protein (lactose-free), magnesium",
"Sleep: Aims for 7+ hours, performance drops under 6 hours",
]
for info in profile_data:
store_user_profile(info)
print(f" Stored: {info[:50]}...")
```
## 6. Log Workout History
```python
print("Logging workout history...")
workouts = [
"Push day: Bench press 3x8 @ 60kg, overhead press 4x12, tricep dips 3x10. Felt strong.",
"Pull day: Deadlift 3x5 @ 80kg, barbell rows 4x10, bicep curls 3x12. Good session.",
"Leg day: Leg press 4x12, hamstring curls 3x12, glute bridges 3x15. Knee felt okay.",
]
for workout in workouts:
print(f" {log_workout(workout)[:60]}...")
print("\nLogging recent meals...")
meals = [
"Post-workout: Whey shake with almond milk, banana, oats",
"Dinner: Grilled chicken, brown rice, steamed vegetables",
"Snack: Greek yogurt (lactose-free) with berries",
]
for meal in meals:
print(f" {log_meal(meal)[:60]}...")
print("\nLogging recovery notes...")
recovery = [
"Slept 7.5 hours, feeling well rested",
"Some DOMS in legs from yesterday, using turmeric milk",
]
for note in recovery:
print(f" {log_recovery(note)[:60]}...")
```
## 7. Talk to Your Fitness Coach
```python
import time
print("=" * 60)
print(" Talking to your fitness coach...")
print("=" * 60)
queries = [
"How much was I lifting for bench press recently?",
"I slept poorly last night (only 5 hours). What should I do for today's workout?",
"Suggest a post-workout meal that works with my dietary restrictions.",
"My knee has been bothering me more. Any exercise modifications?",
]
for query in queries:
print(f"\nUser: {query}")
print("-" * 40)
response = fitness_coach(query)
print(f"Coach: {response}")
time.sleep(1)
```
## 8. Generate Progress Report
```python
print("=" * 60)
print(" Progress Report")
print("=" * 60)
print(get_progress_report())
```
## 9. Try Your Own Query
```python
your_query = "What exercises should I do today?" # Change this!
print(f"You: {your_query}")
print("-" * 40)
print(f"Coach: {fitness_coach(your_query)}")
```
## 10. Cleanup
```python
hindsight.close()
print("Client connection closed.")
```
@@ -1,299 +0,0 @@
---
sidebar_position: 7
---
# Healthcare Assistant with Hindsight Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/healthcare_assistant.ipynb)
:::
A supportive healthcare chatbot that remembers patient history, symptoms, medications, and preferences to provide personalized guidance.
## Disclaimer
**This is a demo application and should NOT be used for actual medical advice. Always consult qualified healthcare professionals.**
## Features
- Tracks symptoms, medications, and allergies
- Maintains patient history across conversations
- Provides health information and wellness tips
- Schedules appointments
## Prerequisites
- OpenAI API key
- Hindsight running locally via Docker (see setup below)
## Start Hindsight Locally
Before running this notebook, start Hindsight in a terminal:
```bash
export OPENAI_API_KEY="your-openai-api-key"
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## 1. Install Dependencies
```python
!pip install -q hindsight-client openai nest-asyncio
```
## 2. Configure OpenAI API Key
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
```python
import getpass
import os
# Set OpenAI API key (used by both Hindsight and the demo)
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
print("API key configured!")
```
## 3. Initialize Clients
```python
import nest_asyncio
nest_asyncio.apply()
from datetime import datetime
import random
from openai import OpenAI
from hindsight_client import Hindsight
# Initialize Hindsight client (connects to local Docker instance)
hindsight = Hindsight(
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
)
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
PATIENT_ID = "patient-demo"
def get_patient_bank_id(patient_id: str) -> str:
return f"patient-{patient_id}"
print("Clients initialized!")
```
## 4. Define Helper Functions
```python
def store_patient_info(patient_id: str, info: str, category: str = "general") -> str:
"""Store patient information."""
bank_id = get_patient_bank_id(patient_id)
today = datetime.now().strftime("%B %d, %Y")
hindsight.retain(
bank_id=bank_id,
content=f"{today} - {category.upper()}: {info}",
metadata={"category": category, "date": today},
)
return f"Recorded {category}: {info}"
def get_patient_history(patient_id: str, query: str) -> str:
"""Retrieve relevant patient history."""
bank_id = get_patient_bank_id(patient_id)
memories = hindsight.recall(
bank_id=bank_id,
query=query,
budget="high",
)
if memories and memories.results:
return "\n".join(f"- {m.text}" for m in memories.results[:10])
return "No relevant history found."
def healthcare_chat(patient_id: str, user_message: str) -> str:
"""Chat with the healthcare assistant."""
bank_id = get_patient_bank_id(patient_id)
history = get_patient_history(
patient_id,
f"symptoms medications allergies conditions {user_message}"
)
system_prompt = f"""You are a supportive healthcare assistant chatbot.
IMPORTANT DISCLAIMERS:
- You are NOT a doctor and cannot provide medical diagnoses
- Always recommend consulting healthcare professionals for serious concerns
- Never prescribe medications or suggest stopping prescribed treatments
Your role:
- Listen empathetically to patient concerns
- Remember and reference their medical history
- Provide general health information and wellness tips
- Help track symptoms over time
- Remind about medications and appointments
- Suggest when to seek professional care
Patient History:
{history}
Guidelines:
- Be warm and supportive
- Ask clarifying questions when needed
- Reference their history when relevant
- Flag any concerning symptoms for professional review"""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
temperature=0.7,
max_tokens=600,
)
answer = response.choices[0].message.content
hindsight.retain(
bank_id=bank_id,
content=f"Patient concern: {user_message}\nGuidance provided: {answer[:200]}...",
metadata={"category": "consultation"},
)
return answer
def get_health_summary(patient_id: str) -> str:
"""Generate a health summary for the patient."""
bank_id = get_patient_bank_id(patient_id)
summary = hindsight.reflect(
bank_id=bank_id,
query="""Summarize this patient's health profile:
1. Known conditions and diagnoses
2. Current medications
3. Allergies and sensitivities
4. Recent symptoms reported
5. Lifestyle factors mentioned
6. Any patterns or trends in their health""",
budget="high",
)
return summary.text if hasattr(summary, 'text') else str(summary)
def schedule_appointment(patient_id: str, appointment_type: str, preferred_time: str) -> str:
"""Schedule an appointment (demo)."""
confirmation_id = f"APT-{random.randint(10000, 99999)}"
store_patient_info(
patient_id,
f"Appointment scheduled: {appointment_type} - Preferred time: {preferred_time} - Confirmation: {confirmation_id}",
category="appointment"
)
return f"Appointment requested: {appointment_type}\nPreferred time: {preferred_time}\nConfirmation ID: {confirmation_id}\n\nA staff member will confirm the exact time within 24 hours."
print("Helper functions defined!")
```
## 5. Set Up Patient Profile
```python
print("Setting up patient profile...")
patient_info = [
("Age: 45, Male, Height: 5'11\", Weight: 185 lbs", "demographics"),
("Allergy: Penicillin - causes hives", "allergies"),
("Allergy: Shellfish - causes throat swelling", "allergies"),
("Current medication: Lisinopril 10mg daily for blood pressure", "medications"),
("Current medication: Metformin 500mg twice daily for Type 2 diabetes", "medications"),
("Condition: Diagnosed with Type 2 diabetes in 2020", "conditions"),
("Condition: Mild hypertension, well-controlled", "conditions"),
("Family history: Father had heart disease", "family_history"),
("Lifestyle: Sedentary job, trying to exercise more", "lifestyle"),
]
for info, category in patient_info:
result = store_patient_info(PATIENT_ID, info, category)
print(f" {result}")
```
## 6. Healthcare Chat
```python
import time
print("=" * 60)
print(" Healthcare Chat")
print("=" * 60)
conversations = [
"Hi, I've been having headaches for the past few days. Should I be worried?",
"The headaches are mostly in the afternoon. I've also been feeling more tired than usual.",
"I've been checking my blood sugar and it's been a bit higher lately, around 140-150 fasting.",
"Can you remind me what allergies I have? I'm going to a new restaurant.",
]
for message in conversations:
print(f"\nPatient: {message}")
print("-" * 40)
response = healthcare_chat(PATIENT_ID, message)
print(f"Assistant: {response}")
time.sleep(1)
```
## 7. Schedule Appointment
```python
print("=" * 60)
print(" Scheduling Appointment")
print("=" * 60)
print(schedule_appointment(PATIENT_ID, "General checkup", "Next Tuesday afternoon"))
```
## 8. Health Summary
```python
print("=" * 60)
print(" Patient Health Summary")
print("=" * 60)
print(get_health_summary(PATIENT_ID))
```
## 9. Try Your Own Question
```python
your_question = "Should I adjust my Metformin dose?" # Change this!
print(f"You: {your_question}")
print("-" * 40)
print(f"Assistant: {healthcare_chat(PATIENT_ID, your_question)}")
```
## 10. Cleanup
```python
hindsight.close()
print("Client connection closed.")
```
@@ -1,187 +0,0 @@
---
sidebar_position: 4
---
# Memory with LiteLLM
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/04-litellm-memory-demo.ipynb)
:::
This notebook demonstrates how to add persistent memory to any LLM app using the `hindsight-litellm` package. Memory storage and injection happen automatically via LiteLLM callbacks - no manual memory management needed!
**Key features demonstrated:**
1. `configure()` + `enable()` - Set up automatic memory integration
2. Automatic storage - Conversations are stored after each LLM call
3. Automatic injection - Relevant memories are injected into prompts
The `hindsight-litellm` package hooks into LiteLLM's callback system to:
- Store each conversation after successful LLM responses
- Inject relevant memories into the system prompt before LLM calls
## Prerequisites
Make sure you have Hindsight running:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- API: http://localhost:8888
- UI: http://localhost:9999
## Installation
```python
!pip install hindsight-litellm litellm nest_asyncio python-dotenv -U -q
```
## Setup
```python
import os
import uuid
import time
import logging
import nest_asyncio
from dotenv import load_dotenv
# Apply nest_asyncio for Jupyter compatibility
nest_asyncio.apply()
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Proxy").setLevel(logging.WARNING)
# Import hindsight_litellm
import hindsight_litellm
# Configuration
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
# Check for API key
if not os.getenv("OPENAI_API_KEY"):
print("Warning: OPENAI_API_KEY not set")
```
## Configure and Enable Automatic Memory
This is all you need! After this, all LiteLLM calls will automatically:
- Have relevant memories injected into the prompt
- Store conversations to Hindsight after the response
```python
# Generate a unique bank_id for this demo session
bank_id = f"demo-{uuid.uuid4().hex[:8]}"
print(f"Using bank_id: {bank_id}")
# Configure and enable hindsight
hindsight_litellm.configure(
hindsight_api_url=HINDSIGHT_API_URL,
bank_id=bank_id,
store_conversations=True, # Automatically store conversations
inject_memories=True, # Automatically inject relevant memories
verbose=True, # Enable logging to debug memory operations
)
hindsight_litellm.enable()
print("Hindsight memory integration enabled!")
```
## Conversation 1: User Introduces Themselves
In this first conversation, the user shares some information about themselves. This will be automatically stored to Hindsight memory.
```python
user_message_1 = "Hi! I'm Alex and I work at Google as a software engineer. I love Python and machine learning."
print(f"User: {user_message_1}\n")
# Use hindsight_litellm.completion() directly
response_1 = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message_1}
],
)
assistant_response_1 = response_1.choices[0].message.content
print(f"Assistant: {assistant_response_1}")
print("\n(Conversation automatically stored to Hindsight)")
```
## Wait for Memory Processing
Hindsight needs a few seconds to process and extract facts from the conversation.
```python
print("Waiting 12 seconds for memory processing...")
time.sleep(12)
print("Done!")
```
## Conversation 2: Test Memory-Augmented Response
Now we start a fresh conversation and ask what the assistant remembers. The memories from the previous conversation will be automatically injected into the prompt!
```python
user_message_2 = "What do you know about me? What programming language should I use for my next project?"
print(f"User: {user_message_2}\n")
# Memories are automatically injected before this call!
response_2 = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message_2}
],
)
print(f"Assistant: {response_2.choices[0].message.content}")
```
## Summary
The assistant should have remembered that Alex:
- Works at Google as a software engineer
- Loves Python and machine learning
And it should have recommended Python based on that knowledge!
```python
print(f"Memories stored in bank: {bank_id}")
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
```
## Cleanup
```python
hindsight_litellm.cleanup()
# Optional: delete the bank
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted bank: {response.json()}")
```
@@ -1,246 +0,0 @@
---
sidebar_position: 8
---
# Movie Recommendation Assistant with Hindsight Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/movie_recommendation.ipynb)
:::
A personalized movie recommender that remembers your preferences, watch history, and tastes to give better suggestions over time.
## Features
- Remembers favorite genres, directors, and actors
- Tracks movies you've watched and enjoyed
- Provides contextual recommendations based on mood
## Prerequisites
- OpenAI API key
- Hindsight running locally via Docker (see setup below)
## Start Hindsight Locally
Before running this notebook, start Hindsight in a terminal:
```bash
export OPENAI_API_KEY="your-openai-api-key"
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## 1. Install Dependencies
```python
!pip install -q hindsight-client openai nest-asyncio
```
## 2. Configure OpenAI API Key
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
```python
import getpass
import os
# Set OpenAI API key (used by both Hindsight and the demo)
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
print("API key configured!")
```
## 3. Initialize Clients
```python
import nest_asyncio
nest_asyncio.apply()
from openai import OpenAI
from hindsight_client import Hindsight
# Initialize Hindsight client (connects to local Docker instance)
hindsight = Hindsight(
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
)
# Initialize OpenAI client
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# Unique identifier for this user's memory bank
USER_ID = "movie-fan-demo"
print("Clients initialized!")
```
## 4. Define Helper Functions
These functions demonstrate the three core Hindsight operations:
- **retain()**: Store memories
- **recall()**: Retrieve relevant memories
- **reflect()**: Synthesize insights from memories
```python
def get_recommendation(user_query: str) -> str:
"""
Get a movie recommendation based on user query and remembered preferences.
"""
# Recall relevant memories about this user's movie preferences
memories = hindsight.recall(
bank_id=USER_ID,
query=f"movie preferences tastes genres {user_query}",
budget="mid",
)
# Build context from memories
memory_context = ""
if memories and memories.results:
memory_context = "\n".join(
f"- {m.text}" for m in memories.results[:5]
)
# Generate recommendation with context
system_prompt = f"""You are a helpful movie recommendation assistant.
You remember the user's preferences and past conversations to give personalized suggestions.
What you know about this user:
{memory_context if memory_context else "No previous preferences recorded yet."}
Give thoughtful, personalized recommendations based on their tastes.
If they mention new preferences, acknowledge them."""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query},
],
temperature=0.7,
max_tokens=500,
)
recommendation = response.choices[0].message.content
# Store this interaction for future context
hindsight.retain(
bank_id=USER_ID,
content=f"User asked: {user_query}\nRecommendation given: {recommendation}",
metadata={"category": "movie_recommendation"},
)
return recommendation
def store_preference(preference: str) -> None:
"""Store an explicit user preference."""
hindsight.retain(
bank_id=USER_ID,
content=f"User preference: {preference}",
metadata={"category": "preference"},
)
print(f"Stored preference: {preference}")
def get_preference_summary() -> str:
"""Get a summary of what we know about the user's movie tastes."""
summary = hindsight.reflect(
bank_id=USER_ID,
query="Summarize this user's movie preferences, favorite genres, actors they like, and movies they've mentioned enjoying or disliking.",
budget="high",
)
return summary.text if hasattr(summary, 'text') else str(summary)
print("Helper functions defined!")
```
## 5. Run the Demo
Watch how the assistant learns and remembers preferences across conversations.
```python
import time
print("=" * 60)
print(" Movie Recommendation Assistant with Memory")
print("=" * 60)
print()
# Simulate a conversation over time
conversations = [
"I'm looking for a movie to watch tonight. Any suggestions?",
"I really loved Inception and Interstellar. Christopher Nolan is amazing!",
"Can you suggest something similar to those? I like mind-bending plots.",
"Actually, I'm not in the mood for something heavy. Something lighter?",
"I watched The Grand Budapest Hotel last week and loved it!",
"What should I watch tonight? Remember what I like!",
]
for i, query in enumerate(conversations, 1):
print(f"\n[Conversation {i}]")
print(f"User: {query}")
print("-" * 40)
response = get_recommendation(query)
print(f"Assistant: {response}")
print()
time.sleep(1)
```
## 6. View Learned Preferences
Use `reflect()` to synthesize what Hindsight has learned about your movie tastes.
```python
print("=" * 60)
print(" What I've learned about your movie tastes:")
print("=" * 60)
print(get_preference_summary())
```
## 7. Try Your Own Queries
Experiment with your own movie preferences!
```python
# Try your own query!
your_query = "I'm in the mood for a sci-fi thriller" # Change this!
print(f"You: {your_query}")
print("-" * 40)
print(f"Assistant: {get_recommendation(your_query)}")
```
## 8. Cleanup
Close the Hindsight client connection.
```python
hindsight.close()
print("Client connection closed.")
```
```python
```
```python
```
@@ -1,247 +0,0 @@
---
sidebar_position: 2
---
# Per-User Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/02-per-user-memory.ipynb)
:::
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, user preferences, and context across sessions.
## The Problem
Without memory, every conversation starts from scratch:
```
Session 1: "I prefer dark mode and use Python"
Session 2: "What's my preferred language?" → Agent doesn't know
```
## The Solution: One Bank Per User
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ User B Bank │ │ User C Bank │
│ │ │ │ │ │
│ - Conversations│ │ - Conversations│ │ - Conversations│
│ - Preferences │ │ - Preferences │ │ - Preferences │
│ - Context │ │ - Context │ │ - Context │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
100% isolated 100% isolated 100% isolated
```
Each user gets their own memory bank. Complete isolation, simple mental model.
```python
!pip install hindsight-client nest_asyncio openai python-dotenv -U
```
## 1. Create a Bank When User Signs Up
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
from openai import OpenAI as OpenAIClient
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
def on_user_signup(user_id: str):
client.create_bank(
bank_id=f"user-{user_id}",
name=f"Memory for {user_id}"
)
print(f"View bank: {HINDSIGHT_UI_URL}/banks/user-{user_id}?view=documents")
```
## 2. Manage Conversation Sessions
Use `document_id` to group messages belonging to the same conversation. When you retain with the same `document_id`, Hindsight replaces the previous version (upsert behavior), keeping the memory up-to-date as the conversation evolves.
```python
import uuid
import json
class ConversationSession:
def __init__(self, user_id: str):
self.user_id = user_id
self.session_id = str(uuid.uuid4()) # Unique ID for this conversation
self.messages = []
def add_message(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
def save(self, client: Hindsight):
"""Save the entire conversation. Replaces previous version if session_id exists."""
# Convert messages to string format for retain
content = "\n".join([f"{m['role']}: {m['content']}" for m in self.messages])
client.retain(
bank_id=f"user-{self.user_id}",
content=content,
document_id=self.session_id # Same ID = upsert (replace old version)
)
```
## 3. Recall Context Before Responding
```python
def get_context(user_id: str, query: str):
result = client.recall(
bank_id=f"user-{user_id}",
query=query
)
return result.results
```
## 4. Complete Agent Loop
```python
def format_results(results):
"""Format recall results for the prompt."""
if not results:
return "No relevant memories found."
return "\n".join([f"- {r.text}" for r in results])
def format_messages(messages):
"""Format conversation messages for the prompt."""
return "\n".join([f"{m['role']}: {m['content']}" for m in messages])
def handle_message(session: ConversationSession, user_message: str):
# 1. Add user message to session
session.add_message("user", user_message)
# 2. Recall relevant context from past conversations
context = client.recall(
bank_id=f"user-{session.user_id}",
query=user_message
)
# 3. Build system prompt with memory
system_prompt = f"""You are a helpful assistant with memory of past conversations.
## What you remember about this user
{format_results(context.results)}
Respond helpfully and reference relevant memories when appropriate."""
# 4. Generate response using OpenAI
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
*[{"role": m["role"], "content": m["content"]} for m in session.messages]
]
)
assistant_response = response.choices[0].message.content
# 5. Add assistant response to session
session.add_message("assistant", assistant_response)
# 6. Save the updated conversation (upserts based on session_id)
session.save(client)
print(f"User: {user_message}")
print(f"Assistant: {assistant_response}\n")
return assistant_response
```
## 5. Starting a New Conversation
```python
# Create the user's bank
on_user_signup("alice")
# Each new conversation gets a new session with a unique ID
session = ConversationSession(user_id="alice")
# Multiple exchanges in the same conversation
handle_message(session, "Hi! I'm working on a Python project")
handle_message(session, "Can you help me with async/await?")
# View the stored conversation in the UI.
# Each message updates the same document (via document_id), so you'll see
# the full conversation history in a single document rather than separate entries.
print(f"\nView documents: {HINDSIGHT_UI_URL}/banks/user-alice?view=documents")
```
## How Document ID Works
The `document_id` parameter is key to managing evolving conversations:
| Scenario | Behavior |
|----------|----------|
| First retain with `document_id="session_123"` | Creates new document |
| Retain again with same `document_id="session_123"` | **Replaces** previous version (upsert) |
| Retain with different `document_id="session_456"` | Creates separate document |
| Retain without `document_id` | Creates new document each time |
This upsert behavior means:
- You always retain the **full conversation** state
- Facts are re-extracted from the complete conversation
- No duplicate or stale facts from old versions
- Memory stays consistent as conversations evolve
## What Gets Remembered
Hindsight automatically extracts and connects:
- **Facts**: "User prefers Python", "User is building a CLI tool"
- **Entities**: People, projects, technologies mentioned
- **Relationships**: How entities relate to each other
- **Temporal context**: When things happened
You don't need to manually extract or structure this - just retain the conversations.
## When to Use This Pattern
**Good fit:**
- Chatbots and assistants
- Personal AI companions
- Any 1:1 user-to-agent interaction
**Consider adding shared knowledge if:**
- You have product docs or FAQs to reference
- Multiple users need access to the same information
- See the Support Agent with Shared Knowledge notebook
## Cleanup
Delete the banks created during this notebook:
```python
import requests
# Delete the user-alice bank
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/user-alice")
print(f"Deleted user-alice: {response.json()}")
```
@@ -1,266 +0,0 @@
---
sidebar_position: 9
---
# Personal AI Assistant with Hindsight Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/personal_assistant.ipynb)
:::
A general-purpose personal assistant that remembers your preferences, schedule, family, work context, and past conversations.
## Features
- Remembers family, work, and personal details
- Tracks preferences and habits
- Helps with scheduling and reminders
- Maintains context across conversations
## Prerequisites
- OpenAI API key
- Hindsight running locally via Docker (see setup below)
## Start Hindsight Locally
Before running this notebook, start Hindsight in a terminal:
```bash
export OPENAI_API_KEY="your-openai-api-key"
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## 1. Install Dependencies
```python
!pip install -q hindsight-client openai nest-asyncio
```
## 2. Configure OpenAI API Key
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
```python
import getpass
import os
# Set OpenAI API key (used by both Hindsight and the demo)
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
print("API key configured!")
```
## 3. Initialize Clients
```python
import nest_asyncio
nest_asyncio.apply()
from datetime import datetime
from openai import OpenAI
from hindsight_client import Hindsight
# Initialize Hindsight client (connects to local Docker instance)
hindsight = Hindsight(
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
)
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
USER_ID = "assistant-user-demo"
print("Clients initialized!")
```
## 4. Define Helper Functions
```python
def remember(info: str, category: str = "general") -> str:
"""Store information to remember."""
today = datetime.now().strftime("%B %d, %Y")
hindsight.retain(
bank_id=USER_ID,
content=f"{today}: {info}",
metadata={"category": category, "date": today},
)
return f"I'll remember: {info}"
def recall_context(query: str) -> str:
"""Recall relevant memories for context."""
memories = hindsight.recall(
bank_id=USER_ID,
query=query,
budget="high",
)
if memories and memories.results:
return "\n".join(f"- {m.text}" for m in memories.results[:8])
return ""
def chat(user_message: str) -> str:
"""Chat with the personal assistant."""
context = recall_context(user_message)
system_prompt = f"""You are a helpful personal AI assistant with long-term memory.
You remember the user's preferences, schedule, family, work context, and past conversations.
What you remember about this user:
{context if context else "No memories recorded yet."}
Your capabilities:
- Remember things when asked ("Remember that...", "Don't forget...")
- Recall past information ("What did I tell you about...", "When is...")
- Provide personalized suggestions based on known preferences
- Help with scheduling and reminders
- Have natural conversations while maintaining context
Be helpful, proactive, and reference relevant memories naturally."""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
temperature=0.7,
max_tokens=500,
)
answer = response.choices[0].message.content
# Check if user is asking to remember something
lower_msg = user_message.lower()
if any(phrase in lower_msg for phrase in ["remember that", "don't forget", "remind me", "note that"]):
hindsight.retain(
bank_id=USER_ID,
content=f"User asked to remember: {user_message}",
metadata={"category": "reminder"},
)
# Store the interaction
hindsight.retain(
bank_id=USER_ID,
content=f"Conversation - User: {user_message[:100]} | Assistant: {answer[:100]}",
metadata={"category": "conversation"},
)
return answer
def get_summary(topic: str = None) -> str:
"""Get a summary of memories."""
query = f"Summarize what you know about {topic}" if topic else \
"Summarize everything you know about this user"
summary = hindsight.reflect(
bank_id=USER_ID,
query=query,
budget="high",
)
return summary.text if hasattr(summary, 'text') else str(summary)
print("Helper functions defined!")
```
## 5. Build Context
```python
print("Building context...")
initial_context = [
("My name is Alex and I work as a product manager at TechCorp", "personal"),
("My wife's name is Sarah and we have two kids: Emma (7) and Jack (4)", "family"),
("I prefer morning meetings and try to keep afternoons for deep work", "preference"),
("My mom's birthday is March 15th", "event"),
("I'm trying to read more - currently reading 'Atomic Habits'", "hobby"),
("I have a weekly team standup every Monday at 10am", "schedule"),
("I'm allergic to cats", "health"),
("My favorite coffee is a flat white with oat milk", "preference"),
("I'm training for a half marathon in April", "goal"),
]
for info, category in initial_context:
result = remember(info, category)
print(f" {result}")
```
## 6. Have a Conversation
```python
import time
print("=" * 60)
print(" Conversation")
print("=" * 60)
conversations = [
"Hey, what's my wife's name again?",
"Remember that my Q1 review is next Thursday at 2pm",
"I need a gift idea for my mom's birthday",
"What time is my Monday standup?",
"Can you recommend a coffee order for me?",
"What books am I reading?",
]
for message in conversations:
print(f"\nAlex: {message}")
print("-" * 40)
response = chat(message)
print(f"Assistant: {response}")
time.sleep(1)
```
## 7. View Summary
```python
print("=" * 60)
print(" What I Know About You")
print("=" * 60)
print(get_summary())
```
```python
print("=" * 60)
print(" Your Family")
print("=" * 60)
print(get_summary("family"))
```
## 8. Try Your Own Message
```python
your_message = "What should I focus on this month with my training?" # Change this!
print(f"You: {your_message}")
print("-" * 40)
print(f"Assistant: {chat(your_message)}")
```
## 9. Cleanup
```python
hindsight.close()
print("Client connection closed.")
```
@@ -1,299 +0,0 @@
---
sidebar_position: 10
---
# Personalized Search Agent with Hindsight Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/personalized_search.ipynb)
:::
A search assistant that learns your preferences, location, dietary needs, and lifestyle to provide contextually relevant search results.
## Features
- Learns location, dietary restrictions, and lifestyle
- Personalizes search queries based on context
- Remembers past searches and preferences
- Integrates with Tavily for real web search (optional)
## Prerequisites
- OpenAI API key
- Hindsight running locally via Docker (see setup below)
- Tavily API key (optional, for real web search)
## Start Hindsight Locally
Before running this notebook, start Hindsight in a terminal:
```bash
export OPENAI_API_KEY="your-openai-api-key"
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## 1. Install Dependencies
```python
# Tavily is optional - demo works with simulated results if not installed
!pip install -q hindsight-client openai tavily-python nest-asyncio
```
## 2. Configure API Keys
Enter your API keys when prompted. Tavily is optional - press Enter to skip for simulated search results.
```python
import getpass
import os
# Set OpenAI API key (used by both Hindsight and the demo)
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
# Tavily is optional - for real web search
if not os.getenv("TAVILY_API_KEY"):
tavily_key = getpass.getpass("Enter your Tavily API key (or press Enter to skip): ")
if tavily_key:
os.environ["TAVILY_API_KEY"] = tavily_key
print("API keys configured!")
```
## 3. Initialize Clients
```python
import nest_asyncio
nest_asyncio.apply()
from openai import OpenAI
from hindsight_client import Hindsight
# Initialize Hindsight client (connects to local Docker instance)
hindsight = Hindsight(
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
)
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# Optional: Tavily for real web search
try:
from tavily import TavilyClient
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
HAS_TAVILY = True
print("Tavily configured - using real web search!")
except (ImportError, Exception) as e:
HAS_TAVILY = False
print("Note: Using simulated search results (Tavily not configured)")
USER_ID = "search-user-demo"
print("Clients initialized!")
```
## 4. Define Helper Functions
```python
def store_preference(preference: str) -> str:
"""Store a user preference."""
hindsight.retain(
bank_id=USER_ID,
content=f"User preference: {preference}",
metadata={"category": "preference"},
)
return f"Learned: {preference}"
def store_interaction(query: str, response: str) -> None:
"""Store a search interaction."""
hindsight.retain(
bank_id=USER_ID,
content=f"Search query: {query}\nResult highlights: {response[:200]}",
metadata={"category": "search_history"},
)
def get_user_context(query: str) -> str:
"""Retrieve relevant user context."""
memories = hindsight.recall(
bank_id=USER_ID,
query=f"preferences location dietary lifestyle {query}",
budget="mid",
)
if memories and memories.results:
return "\n".join(f"- {m.text}" for m in memories.results[:6])
return ""
def personalized_search(query: str) -> str:
"""Perform a personalized search."""
user_context = get_user_context(query)
enhancement_prompt = f"""Given this user's preferences and the search query, suggest how to enhance the search.
User preferences:
{user_context if user_context else "No preferences recorded yet."}
Search query: {query}
Return a JSON object with:
- "enhanced_query": The improved search query incorporating relevant preferences
- "filters": Any specific filters to apply (e.g., "vegetarian", "within 5 miles")
- "reasoning": Brief explanation of personalizations applied"""
enhancement = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": enhancement_prompt}],
temperature=0.3,
max_tokens=300,
)
enhanced_info = enhancement.choices[0].message.content
# Perform the search
if HAS_TAVILY:
search_results = tavily.search(
query=query,
search_depth="advanced",
max_results=5,
)
results_text = "\n".join(
f"- {r['title']}: {r['content'][:150]}..."
for r in search_results.get('results', [])
)
else:
results_text = f"[Simulated search results for: {query}]"
response_prompt = f"""Based on the search results and user preferences, provide a personalized summary.
User preferences:
{user_context if user_context else "No preferences recorded yet."}
Query: {query}
Search enhancement applied:
{enhanced_info}
Search results:
{results_text}
Provide a helpful, personalized response that takes into account their preferences."""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": response_prompt}],
temperature=0.7,
max_tokens=500,
)
answer = response.choices[0].message.content
store_interaction(query, answer)
return answer
def get_preference_profile() -> str:
"""Get a summary of the user's preference profile."""
profile = hindsight.reflect(
bank_id=USER_ID,
query="""Summarize what we know about this user:
- Location and neighborhood
- Dietary preferences and restrictions
- Work style and schedule
- Hobbies and interests
- Family situation
- Shopping preferences""",
budget="high",
)
return profile.text if hasattr(profile, 'text') else str(profile)
print("Helper functions defined!")
```
## 5. Build User Profile
```python
print("Learning user preferences...")
preferences = [
"Lives in San Francisco, Mission District",
"Works remotely as a software engineer",
"Vegetarian, prefers organic food when possible",
"Has a 5-year-old daughter named Emma",
"Enjoys hiking and outdoor activities on weekends",
"Prefers quiet coffee shops for remote work",
"Lactose intolerant, uses oat milk",
"Interested in sustainable and eco-friendly products",
"Usually free on Tuesday and Thursday afternoons",
"Husband is allergic to nuts",
]
for pref in preferences:
result = store_preference(pref)
print(f" {result}")
```
## 6. Personalized Search Results
```python
import time
print("=" * 60)
print(" Personalized Search Results")
print("=" * 60)
searches = [
"Find a good coffee shop for working remotely",
"Restaurant recommendations for a family dinner",
"Birthday gift ideas for a 5-year-old",
]
for query in searches:
print(f"\nSearch: {query}")
print("-" * 40)
result = personalized_search(query)
print(result)
time.sleep(1)
```
## 7. View Preference Profile
```python
print("=" * 60)
print(" User Preference Profile")
print("=" * 60)
print(get_preference_profile())
```
## 8. Try Your Own Search
```python
your_search = "Best hiking trails near me" # Change this!
print(f"Search: {your_search}")
print("-" * 40)
print(personalized_search(your_search))
```
## 9. Cleanup
```python
hindsight.close()
print("Client connection closed.")
```
@@ -1,162 +0,0 @@
---
sidebar_position: 1
---
# Hindsight Quickstart
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/01-quickstart.ipynb)
:::
This notebook covers the basics of using Hindsight:
- **Retain**: Store information in memory
- **Recall**: Retrieve memories matching a query
- **Reflect**: Generate insights from memories
## Prerequisites
Make sure you have Hindsight running. The easiest way is via Docker:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- API: http://localhost:8888
- UI: http://localhost:9999
## Installation
Install the Hindsight Python client:
```python
!pip install hindsight-client nest_asyncio python-dotenv -U
```
## Connect to Hindsight
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
```
## Retain: Store Information
The `retain` operation is used to push new memories into Hindsight. It tells Hindsight to _retain_ the information you pass in.
Behind the scenes, the retain operation uses an LLM to extract key facts, temporal data, entities, and relationships.
```python
# Simple retain
client.retain(
bank_id="my-bank",
content="Alice works at Google as a software engineer"
)
# View the stored document in the UI:
print(f"View documents: {HINDSIGHT_UI_URL}/banks/my-bank?view=documents")
```
```python
# Retain with context and timestamp
client.retain(
bank_id="my-bank",
content="Alice got promoted to senior engineer",
context="career update",
timestamp="2025-06-15T10:00:00Z"
)
```
## Recall: Retrieve Memories
The `recall` operation retrieves memories matching a query. It performs 4 retrieval strategies in parallel:
- **Semantic**: Vector similarity
- **Keyword**: BM25 exact matching
- **Graph**: Entity/temporal/causal links
- **Temporal**: Time range filtering
```python
# Simple recall
results = client.recall(bank_id="my-bank", query="What does Alice do?")
print("Memories:")
for r in results.results:
print(f" - {r.text}")
```
```python
# Temporal recall
results = client.recall(bank_id="my-bank", query="What happened in June?")
print("Memories:")
for r in results.results:
print(f" - {r.text}")
```
## Reflect: Generate Insights
The `reflect` operation performs a more thorough analysis of existing memories. This allows the agent to form new connections between memories which are then persisted as opinions and/or observations.
Example use cases:
- An AI Project Manager reflecting on what risks need to be mitigated
- A Sales Agent reflecting on why certain outreach messages have gotten responses
- A Support Agent reflecting on opportunities where customers have unanswered questions
```python
response = client.reflect(bank_id="my-bank", query="What should I know about Alice?")
print(response)
```
## Memory Types
Hindsight organizes memory into four networks to mimic human memory:
- **World**: Facts about the world ("The stove gets hot")
- **Experiences**: Agent's own experiences ("I touched the stove and it really hurt")
- **Opinion**: Beliefs with confidence scores ("I shouldn't touch the stove again" - .99 confidence)
- **Observation**: Complex mental models derived by reflecting on facts and experiences
## Cleanup
Delete the bank created during this notebook:
```python
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/my-bank")
print(f"Deleted my-bank: {response.json()}")
```
@@ -1,335 +0,0 @@
---
sidebar_position: 11
---
# Study Buddy with Hindsight Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/study_buddy.ipynb)
:::
A personalized study assistant that tracks what you've learned, identifies knowledge gaps, and helps with spaced repetition.
## Features
- Tracks study sessions and topics covered
- Monitors confidence levels per topic
- Identifies knowledge gaps
- Suggests topics for spaced repetition review
## Prerequisites
- OpenAI API key
- Hindsight running locally via Docker (see setup below)
## Start Hindsight Locally
Before running this notebook, start Hindsight in a terminal:
```bash
export OPENAI_API_KEY="your-openai-api-key"
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## 1. Install Dependencies
```python
!pip install -q hindsight-client openai nest-asyncio
```
## 2. Configure OpenAI API Key
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
```python
import getpass
import os
# Set OpenAI API key (used by both Hindsight and the demo)
if not os.getenv("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
print("API key configured!")
```
## 3. Initialize Clients
```python
import nest_asyncio
nest_asyncio.apply()
from datetime import datetime
from openai import OpenAI
from hindsight_client import Hindsight
# Initialize Hindsight client (connects to local Docker instance)
hindsight = Hindsight(
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
)
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
USER_ID = "student-demo"
print("Clients initialized!")
```
## 4. Define Helper Functions
```python
def record_study_session(topic: str, notes: str, confidence: str = "medium") -> str:
"""Record a study session with topic, notes, and self-assessed confidence."""
today = datetime.now().strftime("%B %d, %Y")
content = f"""{today} - STUDY SESSION
Topic: {topic}
Confidence Level: {confidence}
Notes: {notes}"""
hindsight.retain(
bank_id=USER_ID,
content=content,
metadata={
"category": "study_session",
"topic": topic,
"confidence": confidence,
"date": today,
},
)
return f"Recorded study session on '{topic}' (confidence: {confidence})"
def record_question(topic: str, question: str, understood: bool) -> str:
"""Record a question asked during study."""
today = datetime.now().strftime("%B %d, %Y")
content = f"""{today} - QUESTION
Topic: {topic}
Question: {question}
Understood: {"Yes" if understood else "No - needs review"}"""
hindsight.retain(
bank_id=USER_ID,
content=content,
metadata={
"category": "question",
"topic": topic,
"understood": str(understood),
},
)
return f"Recorded question on '{topic}'"
def study_buddy(user_query: str) -> str:
"""Interact with the study buddy."""
memories = hindsight.recall(
bank_id=USER_ID,
query=f"study session topic notes questions {user_query}",
budget="high",
)
memory_context = ""
if memories and memories.results:
memory_context = "\n".join(f"- {m.text}" for m in memories.results[:8])
system_prompt = f"""You are a helpful study buddy and tutor.
You have access to the student's study history, including:
- Topics they've studied and their notes
- Their self-assessed confidence levels
- Questions they've asked and whether they understood the answers
Study History:
{memory_context if memory_context else "No study history recorded yet."}
Your role:
1. Answer questions about topics they're studying
2. Identify knowledge gaps based on their history
3. Suggest topics to review (spaced repetition)
4. Provide encouragement and study tips
5. Connect new concepts to things they've already learned
Be supportive and pedagogical. Reference their previous learning when relevant."""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query},
],
temperature=0.7,
max_tokens=800,
)
answer = response.choices[0].message.content
hindsight.retain(
bank_id=USER_ID,
content=f"Student asked: {user_query}\nExplanation given: {answer[:300]}...",
metadata={"category": "tutoring"},
)
return answer
def get_review_suggestions() -> str:
"""Get suggestions for topics to review."""
suggestions = hindsight.reflect(
bank_id=USER_ID,
query="""Analyze this student's study history and suggest:
1. Topics with low confidence that need more review
2. Topics studied a while ago that should be revisited
3. Questions that weren't fully understood
4. Connections between topics they might have missed
Prioritize by what would most improve their understanding.""",
budget="high",
)
return suggestions.text if hasattr(suggestions, 'text') else str(suggestions)
def get_knowledge_summary(topic: str = None) -> str:
"""Get a summary of what the student knows."""
query = f"Summarize what this student knows about {topic}" if topic else \
"Summarize this student's overall knowledge and progress"
summary = hindsight.reflect(
bank_id=USER_ID,
query=query,
budget="high",
)
return summary.text if hasattr(summary, 'text') else str(summary)
print("Helper functions defined!")
```
## 5. Record Study Sessions
```python
print("Recording study sessions...")
sessions = [
{
"topic": "Classical Mechanics - Newton's Laws",
"notes": "Covered F=ma, action-reaction pairs, inertia. Solved problems on inclined planes.",
"confidence": "high",
},
{
"topic": "Classical Mechanics - Conservation of Momentum",
"notes": "Elastic vs inelastic collisions. Struggled with 2D collision problems.",
"confidence": "low",
},
{
"topic": "Classical Mechanics - Generalized Coordinates",
"notes": "Introduction to Lagrangian mechanics. Degrees of freedom concept.",
"confidence": "medium",
},
{
"topic": "Waves - Simple Harmonic Motion",
"notes": "SHM equations, period, frequency. Connected to springs and pendulums.",
"confidence": "high",
},
{
"topic": "Waves - Frequency Domain",
"notes": "Started Fourier transforms. Math is confusing, need more practice.",
"confidence": "low",
},
]
for session in sessions:
result = record_study_session(**session)
print(f" {result}")
```
## 6. Record Questions
```python
print("Recording questions...")
questions = [
("Conservation of Momentum", "Why is momentum conserved in collisions?", True),
("Conservation of Momentum", "How do I solve 2D collision problems?", False),
("Generalized Coordinates", "What's the advantage of Lagrangian over Newtonian?", True),
("Frequency Domain", "When do I use Fourier transforms vs Laplace?", False),
]
for topic, question, understood in questions:
result = record_question(topic, question, understood)
print(f" {result}")
```
## 7. Interactive Study Session
```python
import time
print("=" * 60)
print(" Study Session")
print("=" * 60)
queries = [
"Can you explain generalized coordinates again? I remember we covered it but I'm fuzzy on the details.",
"What topics should I review before my exam next week?",
"I'm still confused about 2D collision problems. Can you walk me through an example?",
]
for query in queries:
print(f"\nStudent: {query}")
print("-" * 40)
response = study_buddy(query)
print(f"Study Buddy: {response}")
time.sleep(1)
```
## 8. Get Review Suggestions
```python
print("=" * 60)
print(" Recommended Review Topics")
print("=" * 60)
print(get_review_suggestions())
```
## 9. Knowledge Summary
```python
print("=" * 60)
print(" Knowledge Summary")
print("=" * 60)
print(get_knowledge_summary())
```
## 10. Try Your Own Question
```python
your_question = "What are my biggest knowledge gaps right now?" # Change this!
print(f"You: {your_question}")
print("-" * 40)
print(f"Study Buddy: {study_buddy(your_question)}")
```
## 11. Cleanup
```python
hindsight.close()
print("Client connection closed.")
```
@@ -1,315 +0,0 @@
---
sidebar_position: 3
---
# Support Agent with Shared Knowledge
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/03-support-agent-shared-knowledge.ipynb)
:::
This pattern shows how to build a support agent that combines **per-user memory** with **shared product knowledge** (RAG), giving users personalized support while leveraging a single source of truth for documentation.
## The Problem
You're building a support agent that needs to:
- Remember each user's history, preferences, and past issues
- Access shared product documentation
- Keep user data completely isolated from other users
A naive approach would index product docs into each user's memory bank, but this is expensive and wasteful (N copies for N users).
## The Solution: Multi-Bank Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ User B Bank │ │ Shared Docs │
│ │ │ │ │ Bank │
│ - Conversations│ │ - Conversations│ │ │
│ - Preferences │ │ - Preferences │ │ - Product docs │
│ - Past issues │ │ - Past issues │ │ - FAQs │
│ - Solutions │ │ - Solutions │ │ - Guides │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴───────────────────────┘
Agent queries
multiple banks
```
**Key benefits:**
- Product docs indexed once, shared by all users
- User memory is 100% isolated
- Simple mental model, no complex filtering
```python
!pip install hindsight-client nest_asyncio openai python-dotenv -U
```
## 1. Set Up Memory Banks
Create three types of banks:
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
from openai import OpenAI as OpenAIClient
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
# Shared knowledge bank (created once)
shared_bank = client.create_bank(
bank_id="product-docs",
name="Product Documentation"
)
# Per-user banks (created when user signs up)
def create_user_bank(user_id: str):
return client.create_bank(
bank_id=f"user-{user_id}",
name=f"Memory for {user_id}"
)
```
## 2. Index Product Documentation
Index your product docs into the shared bank (do this once, or on doc updates):
```python
# Index product documentation - retain each doc separately
client.retain(
bank_id="product-docs",
content="# Pricing Tiers\n\nBasic: $10/mo, Pro: $25/mo, Enterprise: Contact us"
)
client.retain(
bank_id="product-docs",
content="# Getting Started\n\nTo set up your account, visit the dashboard and click 'New Project'"
)
# View the stored documents in the UI:
print(f"View documents: {HINDSIGHT_UI_URL}/banks/product-docs?view=documents")
```
## 3. Store User Conversations
After each support interaction, retain it in the user's bank:
```python
def save_conversation(user_id: str, messages: list):
# Convert messages to string format
content = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
client.retain(
bank_id=f"user-{user_id}",
content=content
)
```
## 4. Query Multiple Banks at Support Time
When handling a user query, retrieve context from both banks:
```python
def get_support_context(user_id: str, query: str):
# Get user's personal context
user_context = client.recall(
bank_id=f"user-{user_id}",
query=query
)
# Get relevant product documentation
docs_context = client.recall(
bank_id="product-docs",
query=query
)
return {
"user_history": user_context.results,
"documentation": docs_context.results
}
```
## 5. Build the Agent Prompt
Combine both contexts in your agent's prompt:
```python
def format_results(results):
"""Format recall results for the prompt."""
if not results:
return "No relevant information found."
return "\n".join([f"- {r.text}" for r in results])
def build_prompt(query: str, context: dict) -> str:
return f"""You are a helpful support agent.
## User's History
{format_results(context["user_history"])}
## Product Documentation
{format_results(context["documentation"])}
## Current Question
{query}
Use the user's history to personalize your response and the documentation
for accurate product information. If you find a solution, remember it for
future reference.
"""
```
## Promoting Learnings to Shared Knowledge
When the agent discovers a solution that's not in the docs, you can optionally promote it to a "learnings" bank:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ Shared Docs │ │ Learnings │
│ │ │ Bank │ │ Bank │
│ - Conversations│ │ │ │ │
│ - Preferences │ │ - Product docs │ │ - Verified │
│ - Past issues │ │ - FAQs │ │ solutions │
│ - Solutions │ │ - Guides │ │ - Workarounds │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴───────────────────────┘
Agent queries
all three banks
```
```python
# Optional: Create a curated learnings bank
learnings_bank = client.create_bank(
bank_id="support-learnings",
name="Curated Support Learnings"
)
# After a successful resolution
def promote_learning(insight: str):
client.retain(
bank_id="support-learnings",
content=insight
)
```
## Complete Example
```python
def format_results(results):
if not results:
return "No relevant information found."
return "\n".join([f"- {r.text}" for r in results])
def handle_support_request(user_id: str, query: str):
# 1. Recall from user's memory
user_recall = client.recall(
bank_id=f"user-{user_id}",
query=query
)
# 2. Recall from shared docs
docs_recall = client.recall(
bank_id="product-docs",
query=query
)
# 3. Recall from learnings (optional)
learnings_recall = client.recall(
bank_id="support-learnings",
query=query
)
# 4. Build system prompt with context
system_prompt = f"""You are a helpful support agent. Use the context below to answer the user's question.
## User's History
{format_results(user_recall.results)}
## Product Documentation
{format_results(docs_recall.results)}
## Known Solutions
{format_results(learnings_recall.results)}
Provide helpful, accurate responses based on the documentation. Reference the user's history when relevant."""
# 5. Generate response using OpenAI
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
]
)
assistant_response = response.choices[0].message.content
# 6. Save the conversation to user's memory
conversation = f"user: {query}\nassistant: {assistant_response}"
client.retain(
bank_id=f"user-{user_id}",
content=conversation
)
return assistant_response
# Test the function
create_user_bank("bob")
print("User: How do I get started?")
result = handle_support_request("bob", "How do I get started?")
print(f"Assistant: {result}")
print(f"\nView user memory: {HINDSIGHT_UI_URL}/banks/user-bob?view=documents")
```
## When to Use This Pattern
**Good fit:**
- Support agents with shared documentation
- Multi-tenant applications with shared reference data
- Any scenario needing user isolation + shared knowledge
**Consider alternatives if:**
- You need cross-user learning (users benefiting from other users' solutions)
- Entity relationships must span across users and docs
## Cleanup
Delete the banks created during this notebook:
```python
import requests
# Delete all banks created in this notebook
for bank_id in ["product-docs", "support-learnings", "user-bob"]:
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted {bank_id}: {response.json()}")
```
@@ -1,372 +0,0 @@
---
sidebar_position: 5
---
# Routing Tool Learning
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/05-tool-learning-demo.ipynb)
:::
This notebook demonstrates how Hindsight helps an LLM learn which tool to use when tool names are ambiguous. Without memory, the LLM might randomly select between similarly-named tools. With Hindsight, it learns from past interactions and consistently makes the correct choice.
## The Scenario
We have a task routing system with two tools:
- `route_to_channel_alpha` - Routes to processing channel Alpha
- `route_to_channel_omega` - Routes to processing channel Omega
The tool names and descriptions are **intentionally vague**. In reality:
- Channel Alpha handles **FINANCIAL/PAYMENT** tasks (refunds, billing, etc.)
- Channel Omega handles **TECHNICAL/SUPPORT** tasks (bugs, features, etc.)
**Without Hindsight:** The LLM guesses randomly based on vague descriptions
**With Hindsight:** The LLM learns from feedback which channel handles what
## Prerequisites
Make sure you have Hindsight running:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## Installation
```python
!pip install hindsight-litellm hindsight-client litellm nest_asyncio python-dotenv -U -q
```
## Setup
```python
import os
import json
import uuid
import time
import logging
import nest_asyncio
from typing import Optional
from dotenv import load_dotenv
nest_asyncio.apply()
load_dotenv()
logging.basicConfig(level=logging.INFO)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
import litellm
import hindsight_litellm
from hindsight_client import Hindsight
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
if not os.getenv("OPENAI_API_KEY"):
print("Warning: OPENAI_API_KEY not set")
```
## Define Tools
These tool definitions are **intentionally ambiguous** - the descriptions don't reveal which channel handles what type of request.
```python
TOOLS = [
{
"type": "function",
"function": {
"name": "route_to_channel_alpha",
"description": "Routes the customer request to processing channel Alpha. Use this channel for appropriate request types.",
"parameters": {
"type": "object",
"properties": {
"request_summary": {
"type": "string",
"description": "A brief summary of the customer's request"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the request"
}
},
"required": ["request_summary"]
}
}
},
{
"type": "function",
"function": {
"name": "route_to_channel_omega",
"description": "Routes the customer request to processing channel Omega. Use this channel for appropriate request types.",
"parameters": {
"type": "object",
"properties": {
"request_summary": {
"type": "string",
"description": "A brief summary of the customer's request"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the request"
}
},
"required": ["request_summary"]
}
}
}
]
```
## Test Scenarios
A mix of financial and technical requests to test routing accuracy.
```python
TEST_SCENARIOS = [
{
"type": "financial",
"request": "I was charged twice for my subscription last month. I need a refund for the duplicate charge.",
"correct_tool": "route_to_channel_alpha"
},
{
"type": "technical",
"request": "The app keeps crashing when I try to upload a file larger than 10MB. This bug is blocking my work.",
"correct_tool": "route_to_channel_omega"
},
{
"type": "financial",
"request": "My invoice shows an incorrect amount. The billing department needs to fix this.",
"correct_tool": "route_to_channel_alpha"
},
{
"type": "technical",
"request": "I'd like to request a new feature: the ability to export reports as PDF.",
"correct_tool": "route_to_channel_omega"
},
{
"type": "financial",
"request": "I need to update my payment method and understand why my last payment failed.",
"correct_tool": "route_to_channel_alpha"
},
]
```
## Helper Functions
```python
SYSTEM_PROMPT = """You are a customer service routing agent. Your job is to route customer requests to the appropriate processing channel.
You have access to two routing channels:
- route_to_channel_alpha: Routes to channel Alpha
- route_to_channel_omega: Routes to channel Omega
Analyze the customer's request and route it to the most appropriate channel. You must call one of the routing functions to process the request.
Important: Base your routing decision on what you know about each channel's purpose. If you have learned from previous interactions which channel handles specific types of requests, use that knowledge."""
def make_routing_request(user_request: str, use_hindsight: bool, bank_id: Optional[str] = None):
"""Make a routing request and return the tool called."""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Customer Request: {user_request}"}
]
if use_hindsight and bank_id:
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="required",
temperature=0.0,
)
else:
response = litellm.completion(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="required",
temperature=0.7,
)
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
return tool_call.function.name
return None
def store_feedback(bank_id: str, request: str, correct_tool: str, request_type: str):
"""Store feedback about which tool was correct for a request type."""
client = Hindsight(base_url=HINDSIGHT_API_URL, timeout=60.0)
feedback_content = f"""ROUTING FEEDBACK:
Request type: {request_type}
Customer request: "{request}"
Correct routing: {correct_tool}
LEARNED RULE: {request_type.upper()} requests (like refunds, billing, payments, charges, invoices) should ALWAYS be routed to {correct_tool}.
This is important institutional knowledge for routing decisions."""
client.retain(
bank_id=bank_id,
content=feedback_content,
context=f"routing:feedback:{request_type}",
metadata={"request_type": request_type, "correct_tool": correct_tool}
)
```
## Phase 1: Without Hindsight (No Memory)
The LLM has no prior knowledge about which channel handles what. With ambiguous tool descriptions, it may route incorrectly.
```python
print("=" * 60)
print("PHASE 1: WITHOUT HINDSIGHT (No Memory)")
print("=" * 60)
phase1_results = []
for i, scenario in enumerate(TEST_SCENARIOS[:3], 1):
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
print(f"Request: \"{scenario['request'][:60]}...\"")
tool_name = make_routing_request(scenario['request'], use_hindsight=False)
is_correct = tool_name == scenario['correct_tool']
phase1_results.append(is_correct)
print(f"LLM chose: {tool_name}")
print(f"Correct tool: {scenario['correct_tool']}")
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
phase1_accuracy = sum(phase1_results) / len(phase1_results) * 100
print(f"\n>>> Phase 1 Accuracy: {phase1_accuracy:.0f}% ({sum(phase1_results)}/{len(phase1_results)})")
```
## Phase 2: Teaching Phase
Now we provide feedback about correct routing to build memory. This simulates a human supervisor correcting the AI's routing decisions.
```python
bank_id = f"tool-learning-{uuid.uuid4().hex[:8]}"
print(f"Using bank_id: {bank_id}")
# Configure and enable Hindsight
hindsight_litellm.configure(
hindsight_api_url=HINDSIGHT_API_URL,
bank_id=bank_id,
store_conversations=True,
inject_memories=True,
max_memories=10,
recall_budget="high",
verbose=False,
)
hindsight_litellm.enable()
print("\nStoring routing feedback...")
feedback_examples = [
("I need a refund for an incorrect charge on my account.", "route_to_channel_alpha", "financial"),
("There's a bug in the system causing data loss.", "route_to_channel_omega", "technical"),
("My billing statement has errors that need correction.", "route_to_channel_alpha", "financial"),
("I want to request a new feature for the dashboard.", "route_to_channel_omega", "technical"),
]
for request, correct_tool, req_type in feedback_examples:
print(f" Storing: {req_type.upper()}{correct_tool}")
store_feedback(bank_id, request, correct_tool, req_type)
print("\nWaiting 15 seconds for Hindsight to process memories...")
time.sleep(15)
print("Done!")
```
## Phase 3: With Hindsight (Memory-Augmented)
The LLM now has access to learned routing knowledge via Hindsight. It should route requests correctly based on past feedback.
```python
print("=" * 60)
print("PHASE 3: WITH HINDSIGHT (Memory-Augmented)")
print("=" * 60)
phase3_results = []
for i, scenario in enumerate(TEST_SCENARIOS, 1):
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
print(f"Request: \"{scenario['request'][:60]}...\"")
tool_name = make_routing_request(
scenario['request'],
use_hindsight=True,
bank_id=bank_id
)
is_correct = tool_name == scenario['correct_tool']
phase3_results.append(is_correct)
print(f"LLM chose: {tool_name}")
print(f"Correct tool: {scenario['correct_tool']}")
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
phase3_accuracy = sum(phase3_results) / len(phase3_results) * 100
print(f"\n>>> Phase 3 Accuracy: {phase3_accuracy:.0f}% ({sum(phase3_results)}/{len(phase3_results)})")
```
## Summary
```python
print("=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"\nPhase 1 (No Memory): {phase1_accuracy:.0f}% accuracy")
print(f"Phase 3 (With Hindsight): {phase3_accuracy:.0f}% accuracy")
improvement = phase3_accuracy - phase1_accuracy
if improvement > 0:
print(f"\n🎉 Improvement: +{improvement:.0f}% accuracy with Hindsight!")
elif improvement == 0:
print(f"\nNote: Results may vary. Run again to see learning effect.")
else:
print(f"\nNote: Phase 1 got lucky! Run again to see typical behavior.")
print(f"\nMemories stored in bank: {bank_id}")
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
print("\n" + "=" * 60)
print("KEY INSIGHT")
print("=" * 60)
print("Hindsight allows the LLM to learn from experience which tool")
print("to use, even when tool names/descriptions are ambiguous.")
```
## Cleanup
```python
hindsight_litellm.cleanup()
# Optional: delete the bank
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted bank: {response.json()}")
```
@@ -187,56 +187,5 @@
}
]
}
],
"cookbookSidebar": [
{
"type": "doc",
"id": "cookbook/index",
"label": "Overview"
},
{
"type": "category",
"label": "Recipes",
"collapsible": false,
"items": [
{
"type": "doc",
"id": "cookbook/recipes/quickstart",
"label": "Hindsight Quickstart"
},
{
"type": "doc",
"id": "cookbook/recipes/per-user-memory",
"label": "Per-User Memory"
},
{
"type": "doc",
"id": "cookbook/recipes/support-agent-shared-knowledge",
"label": "Support Agent with Shared Knowledge"
},
{
"type": "doc",
"id": "cookbook/recipes/litellm-memory-demo",
"label": "Memory with LiteLLM"
},
{
"type": "doc",
"id": "cookbook/recipes/tool-learning-demo",
"label": "Routing Tool Learning"
}
]
},
{
"type": "category",
"label": "Applications",
"collapsible": false,
"items": [
{
"type": "doc",
"id": "cookbook/applications/openai-fitness-coach",
"label": "OpenAI Agent + Hindsight Memory Integration"
}
]
}
]
}
}
@@ -231,12 +231,5 @@
}
]
}
],
"cookbookSidebar": [
{
"type": "doc",
"id": "cookbook/index",
"label": "Cookbook"
}
]
}
}