Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2572331c |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ` || ''})`;
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+24
-16
@@ -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 |
-315
@@ -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()}")
|
||||
```
|
||||
-315
@@ -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)
|
||||
-206
@@ -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
|
||||
-123
@@ -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
|
||||
-315
@@ -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
|
||||
-371
@@ -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.")
|
||||
```
|
||||
-315
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user