From 66530146b7780903a7b34de139d288f80b4dae12 Mon Sep 17 00:00:00 2001 From: nftchance Date: Fri, 7 Feb 2025 11:18:53 -0600 Subject: [PATCH 1/6] feat: base chat column --- .../app/components/app/columns/column.tsx | 3 + .../app/columns/utils/column-add.tsx | 11 +- .../app/columns/utils/column-chat.tsx | 145 ++++++++++++++++++ packages/app/state/columns.ts | 1 + 4 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 packages/app/components/app/columns/utils/column-chat.tsx diff --git a/packages/app/components/app/columns/column.tsx b/packages/app/components/app/columns/column.tsx index 242cbab0f..d88403e4d 100644 --- a/packages/app/components/app/columns/column.tsx +++ b/packages/app/components/app/columns/column.tsx @@ -23,6 +23,7 @@ import { COLUMNS, useColumnStore } from "@/state/columns" import { usePlugStore } from "@/state/plugs" import { SparklingText } from "../utils/sparkling-text" +import { ColumnChat } from "./utils/column-chat" const MIN_COLUMN_WIDTH = 420 const MAX_COLUMN_WIDTH = 680 @@ -247,6 +248,8 @@ export const ConsoleColumn: FC<{
{column.key === COLUMNS.KEYS.ADD ? ( + ) : column.key === COLUMNS.KEYS.CHAT ? ( + ) : column.key === COLUMNS.KEYS.DISCOVER ? ( ) : column.key === COLUMNS.KEYS.MY_PLUGS ? ( diff --git a/packages/app/components/app/columns/utils/column-add.tsx b/packages/app/components/app/columns/utils/column-add.tsx index 469c21b9f..1349372fa 100644 --- a/packages/app/components/app/columns/utils/column-add.tsx +++ b/packages/app/components/app/columns/utils/column-add.tsx @@ -1,7 +1,6 @@ -import Link from "next/link" import { useMemo } from "react" -import { Activity, Cable, Cog, Coins, ExternalLink, Globe, ImageIcon, PiggyBank, Plug, Plus, Star, LockIcon } from "lucide-react" +import { Activity, Cable, Cog, Coins, Globe, ImageIcon, PiggyBank, Plug, Plus, Star, LockIcon, MessageCircle } from "lucide-react" import { Accordion } from "@/components/shared/utils/accordion" import { cn, formatTitle } from "@/lib" @@ -26,6 +25,11 @@ export const ANONYMOUS_OPTIONS: Options = [ label: "MY_PLUGS", description: "Create, edit, and run your Plugs.", icon: + }, + { + label: "CHAT", + description: "Chat with Biblo to help build your Plugs.", + icon: } ] @@ -68,7 +72,8 @@ export const ColumnAdd = ({ index }: { index: number }) => { description: "Install Plug as an app on your device.", icon: }) - + + if (socket?.admin) { options.push({ label: "ADMIN", diff --git a/packages/app/components/app/columns/utils/column-chat.tsx b/packages/app/components/app/columns/utils/column-chat.tsx new file mode 100644 index 000000000..86aabf05e --- /dev/null +++ b/packages/app/components/app/columns/utils/column-chat.tsx @@ -0,0 +1,145 @@ +import { Button } from "@/components/shared/buttons/button" +import { useState } from "react" +import { Search } from "../../inputs/search" +import { SearchIcon } from "lucide-react" +import { cn, formatTitle } from "@/lib"; +import { Counter } from "@/components/shared/utils/counter"; + +interface Message { + text: string; + isSent: boolean; +} + +const TypingIndicator = () => { + return ( +
+
+
+
+
+
+
+ ); +}; + +const DEFAULT_MESSAGES = [ + "Let's create a Plug.", + "How are my Plugs doing?", + "Where can I use my holdings?" +] + +const TOOLS = ["schemas", "schema", "holdings", "price", "plugs", "plug", "simulate"] + +export const ColumnChat = ({ }: { index: number }) => { + const [message, setMessage] = useState("") + const [messages, setMessages] = useState([{ + text: "👋 Hey, I'm Biblo. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited for context I can't go find in the folder I have for you.", + isSent: false + }]) + const [isTyping, setIsTyping] = useState(false) + const [activeTools, setActiveTools] = useState([]) + + const addRandomTools = () => { + const availableTools = TOOLS.filter(tool => !activeTools.includes(tool)) + const numToolsToAdd = Math.min(Math.floor(Math.random() * 2) + 1, availableTools.length) + const shuffled = [...availableTools].sort(() => 0.5 - Math.random()) + const newTools = shuffled.slice(0, numToolsToAdd) + setActiveTools(prev => [...prev, ...newTools]) + } + + const handleSubmit = (sent: string) => { + if (!sent.trim()) return; + + setMessages([...messages, { text: sent, isSent: true }]) + setIsTyping(true) + + setTimeout(() => { + setIsTyping(false) + setMessages(prev => [...prev, { text: "This is a reply", isSent: false }]) + addRandomTools() + }, 1000) + + setMessage("") + } + + return <> +
+
+ + +
+ {isTyping && } + {[...messages].reverse().map((msg, index) => ( +
+
+ {msg.text} +
+
+ ))} +
+ +
+ {activeTools.length > 0 && ( +
+ +
+ {activeTools.map((tool, index) => ( + + ))} +
+
+ )} + +
+ {DEFAULT_MESSAGES.map((msg, index) => ( + + ))} +
+ + } + search={message} + handleSearch={message => setMessage(message)} + placeholder="Type a message..." + className="message-input" + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(message); + } + }} + /> + +
+
+ +} diff --git a/packages/app/state/columns.ts b/packages/app/state/columns.ts index d691be511..efd2fb8dc 100644 --- a/packages/app/state/columns.ts +++ b/packages/app/state/columns.ts @@ -13,6 +13,7 @@ export const COLUMNS = { KEYS: { ACTIVITY: "ACTIVITY", ADD: "ADD", + CHAT: "CHAT", ADMIN: "ADMIN", ALERTS: "ALERTS", APPLICATION: "APPLICATION", From f68bda9015ed8dcaf5e1d2700e8da6c8137cb765 Mon Sep 17 00:00:00 2001 From: nftchance Date: Fri, 7 Feb 2025 13:51:55 -0600 Subject: [PATCH 2/6] chore: last commit of the day for this branch --- .../app/columns/utils/column-chat.tsx | 50 ++++--- packages/app/lib/tools.ts | 30 ++++ packages/app/server/api/root.ts | 2 + packages/app/server/api/routers/biblo/chat.ts | 131 ++++++++++++++++++ .../app/server/api/routers/biblo/index.ts | 7 + 5 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 packages/app/lib/tools.ts create mode 100644 packages/app/server/api/routers/biblo/chat.ts create mode 100644 packages/app/server/api/routers/biblo/index.ts diff --git a/packages/app/components/app/columns/utils/column-chat.tsx b/packages/app/components/app/columns/utils/column-chat.tsx index 86aabf05e..80fb7c408 100644 --- a/packages/app/components/app/columns/utils/column-chat.tsx +++ b/packages/app/components/app/columns/utils/column-chat.tsx @@ -4,6 +4,7 @@ import { Search } from "../../inputs/search" import { SearchIcon } from "lucide-react" import { cn, formatTitle } from "@/lib"; import { Counter } from "@/components/shared/utils/counter"; +import { api } from "@/server/client"; interface Message { text: string; @@ -28,8 +29,6 @@ const DEFAULT_MESSAGES = [ "Where can I use my holdings?" ] -const TOOLS = ["schemas", "schema", "holdings", "price", "plugs", "plug", "simulate"] - export const ColumnChat = ({ }: { index: number }) => { const [message, setMessage] = useState("") const [messages, setMessages] = useState([{ @@ -39,27 +38,38 @@ export const ColumnChat = ({ }: { index: number }) => { const [isTyping, setIsTyping] = useState(false) const [activeTools, setActiveTools] = useState([]) - const addRandomTools = () => { - const availableTools = TOOLS.filter(tool => !activeTools.includes(tool)) - const numToolsToAdd = Math.min(Math.floor(Math.random() * 2) + 1, availableTools.length) - const shuffled = [...availableTools].sort(() => 0.5 - Math.random()) - const newTools = shuffled.slice(0, numToolsToAdd) - setActiveTools(prev => [...prev, ...newTools]) - } + const chat = api.biblo.chat.message.useMutation() - const handleSubmit = (sent: string) => { + const handleSubmit = async (sent: string) => { if (!sent.trim()) return; - setMessages([...messages, { text: sent, isSent: true }]) - setIsTyping(true) - - setTimeout(() => { - setIsTyping(false) - setMessages(prev => [...prev, { text: "This is a reply", isSent: false }]) - addRandomTools() - }, 1000) - - setMessage("") + setMessages([...messages, { text: sent, isSent: true }]); + setIsTyping(true); + setMessage(""); + + try { + const response = await chat.mutateAsync({ + message: sent, + // tools: activeTools, + history: messages.map(msg => ({ + content: msg.text, + role: msg.isSent ? 'user' : 'assistant' + })) + }); + + if (response.tools?.length) { + setActiveTools(prev => [...new Set([...prev, ...response.tools])]); + } + setMessages(prev => [...prev, { text: response.reply, isSent: false }, ...response.additionalMessages.map(text => ({ text, isSent: false }))]); + } catch (error) { + console.error('Error:', error); + setMessages(prev => [...prev, { + text: "Sorry, I encountered an error. Please try again.", + isSent: false + }]); + } finally { + setIsTyping(false); + } } return <> diff --git a/packages/app/lib/tools.ts b/packages/app/lib/tools.ts new file mode 100644 index 000000000..d2f0a8654 --- /dev/null +++ b/packages/app/lib/tools.ts @@ -0,0 +1,30 @@ + +export type ToolFunction = { + name: string; + description: string; + execute: (...args: any[]) => Promise; +} + +export const TOOLS: Record = { + "schemas": { + name: "schemas", + description: "List all available schemas in the system", + execute: async () => { + return ["User", "Plug", "Holdings"]; + } + }, + "holdings": { + name: "holdings", + description: "Get current holdings information", + execute: async () => { + return { /* holdings data */ }; + } + }, + "price": { + name: "price", + description: "Get current price information for a specific item", + execute: async (itemId: string) => { + return { /* price data */ }; + } + }, +} diff --git a/packages/app/server/api/root.ts b/packages/app/server/api/root.ts index 57b976a88..2733c82c3 100644 --- a/packages/app/server/api/root.ts +++ b/packages/app/server/api/root.ts @@ -1,5 +1,6 @@ import { inferRouterInputs, inferRouterOutputs } from "@trpc/server" +import { biblo } from "@/server/api/routers/biblo" import { jobs } from "@/server/api/routers/jobs" import { misc } from "@/server/api/routers/misc" import { plugs } from "@/server/api/routers/plugs" @@ -8,6 +9,7 @@ import { solver } from "@/server/api/routers/solver" import { createTRPCRouter } from "@/server/api/trpc" export const appRouter = createTRPCRouter({ + biblo, plugs, socket, jobs, diff --git a/packages/app/server/api/routers/biblo/chat.ts b/packages/app/server/api/routers/biblo/chat.ts new file mode 100644 index 000000000..e47c7aff5 --- /dev/null +++ b/packages/app/server/api/routers/biblo/chat.ts @@ -0,0 +1,131 @@ +import { z } from "zod" +import { Anthropic } from '@anthropic-ai/sdk' +import { createTRPCRouter, protectedProcedure } from "../../trpc" +import { env } from "@/env" +import { TOOLS } from "@/lib/tools" +import { schemas } from "@/lib" +import { TRPCError } from "@trpc/server" + +const CLAUDE_MODEL = "claude-3-haiku-20240307" + +const anthropic = new Anthropic({ + apiKey: env.ANTHROPIC_KEY +}) + +const messageSchema = z.object({ + message: z.string(), + history: z.array(z.object({ + content: z.string(), + role: z.enum(['user', 'assistant']) + })) +}) + +interface MessageResponse { + reply: string; + additionalMessages?: string[]; + tools: string[]; +} + +export const chat = createTRPCRouter({ + message: protectedProcedure + .input(messageSchema) + .mutation(async ({ ctx, input }) => { + const socket = await ctx.db.userSocket.findUnique({ where: { id: ctx.session.user.id } }) + if (!socket) throw new TRPCError({ code: "NOT_FOUND" }) + + const messages = input.history.map(msg => ({ + role: msg.role, + content: msg.content + })) + + messages.push({ + role: 'user', + content: input.message + }) + + const initialResponse = await anthropic.messages.create({ + model: CLAUDE_MODEL, + max_tokens: 1024, + messages, + system: `You are Biblo, a founder of Plug that is the user-facing member helping them be as successful as possible. You have pride in your work and want them to use and succeed. + Plug is an if-this-then-that like system that uses actions defined in schemas to build workflows. + They build a workflow in Plug and have everything automatically executed. Your purpose is to get them to build effective and profitable Plugs. + This is very important, users do not use different apps and we do not send them anywhere else. We recommend they build a Plug when we have that protocol schema supported. + Available tools: ${Object.keys(TOOLS).map(toolKey => `${TOOLS[toolKey].name}: ${TOOLS[toolKey].description}`)}. + When appropriate, suggest relevant tools from this list. + You can provide multiple messages by using Claude's natural break points in your response. + You do not address or acknowledge the messages providing you data in tags. + Current user: ${ctx.session.user.id} + `, + }) + const initialReply = initialResponse.content[0].type === 'text' ? initialResponse.content[0].text : '' + const neededTools = parseToolSuggestions(initialReply) + if (neededTools.length > 0) { + const toolResults = await executeTools(socket.socketAddress, neededTools) + const finalResponse = await anthropic.messages.create({ + model: CLAUDE_MODEL, + max_tokens: 1024, + messages: [ + ...messages, + { + role: 'assistant', + content: initialReply + }, + { + role: 'user', + content: `The results from the tools you requested are: ${JSON.stringify(toolResults, null, 2)}\nPlease provide a concise follow up if needed.` + } + ], + system: `You are Biblo, a helpful assistant for the Plug platform. + Keep responses clear and concise. + Current user: ${ctx.session.user.id}`, + }) + + const finalMessages = finalResponse.content + .filter(content => content.type === 'text') + .map(content => content.text) + + return { + reply: finalMessages[0], + additionalMessages: finalMessages.slice(1), + tools: neededTools + } + } + + const finalMessages = initialResponse.content + .filter(content => content.type === 'text') + .map(content => content.text) + + return { + reply: finalMessages[0], + additionalMessages: finalMessages.slice(1), + tools: neededTools + } + }) +}) + +async function executeTools(socketAddress: string, tools: string[]) { + const results: Record = {} + for (const tool of tools) { + switch (tool) { + case 'holdings': + results.holdings = [{ "name": "USDC", "value": 123456789 }] + break + case 'schemas': + results.schemas = await schemas(undefined, undefined, 8453, socketAddress) + break + case 'schema': + results.schemas = await schemas(undefined, undefined, 8453, socketAddress) + break + } + } + return results +} + +function parseToolSuggestions(text: string | undefined): string[] { + if (!text) return [] + const tools = ['holdings', 'schemas', 'price'] + return tools.filter(tool => + text.toLowerCase().includes(tool.toLowerCase()) + ) +} diff --git a/packages/app/server/api/routers/biblo/index.ts b/packages/app/server/api/routers/biblo/index.ts new file mode 100644 index 000000000..acec1ba74 --- /dev/null +++ b/packages/app/server/api/routers/biblo/index.ts @@ -0,0 +1,7 @@ + +import { createTRPCRouter } from "../../trpc" +import { chat } from "./chat" + +export const biblo = createTRPCRouter({ + chat, +}) From 7e6da390d47032090a315552b71e070e9102c48f Mon Sep 17 00:00:00 2001 From: Drake Danner Date: Mon, 10 Feb 2025 21:56:06 -0500 Subject: [PATCH 3/6] crashout: check point before i crashout even more Co-authored-by: CHANCE --- .../app/columns/utils/column-chat.tsx | 31 ++- packages/app/lib/functions/plug/solver.ts | 36 ++- packages/app/lib/functions/posts.ts | 2 +- .../app/lib/functions/zerion/positions.ts | 8 +- packages/app/lib/tools.ts | 227 +++++++++++++++-- packages/app/prisma/dbml/schema.dbml | 15 +- .../migrations/20250210201603_/migration.sql | 13 + .../migrations/20250210212010_/migration.sql | 2 + packages/app/prisma/schema.prisma | 20 ++ packages/app/server/api/routers/biblo/chat.ts | 241 +++++++++++++----- packages/app/state/positions.ts | 6 + 11 files changed, 494 insertions(+), 107 deletions(-) create mode 100644 packages/app/prisma/migrations/20250210201603_/migration.sql create mode 100644 packages/app/prisma/migrations/20250210212010_/migration.sql diff --git a/packages/app/components/app/columns/utils/column-chat.tsx b/packages/app/components/app/columns/utils/column-chat.tsx index 80fb7c408..501ab0df5 100644 --- a/packages/app/components/app/columns/utils/column-chat.tsx +++ b/packages/app/components/app/columns/utils/column-chat.tsx @@ -1,5 +1,5 @@ import { Button } from "@/components/shared/buttons/button" -import { useState } from "react" +import { useState, useEffect } from "react" import { Search } from "../../inputs/search" import { SearchIcon } from "lucide-react" import { cn, formatTitle } from "@/lib"; @@ -24,15 +24,16 @@ const TypingIndicator = () => { }; const DEFAULT_MESSAGES = [ - "Let's create a Plug.", - "How are my Plugs doing?", - "Where can I use my holdings?" + "Help me open an Aave V3 position", + "What can I do with my tokens?", + "What can I do with Plug?", ] -export const ColumnChat = ({ }: { index: number }) => { +export const ColumnChat = ({ index }: { index: number }) => { + const [message, setMessage] = useState("") - const [messages, setMessages] = useState([{ - text: "👋 Hey, I'm Biblo. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited for context I can't go find in the folder I have for you.", + const [messages, setMessages] = useState(() => [{ + text: "👋 Hey, I'm Piggy. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited.", isSent: false }]) const [isTyping, setIsTyping] = useState(false) @@ -50,7 +51,6 @@ export const ColumnChat = ({ }: { index: number }) => { try { const response = await chat.mutateAsync({ message: sent, - // tools: activeTools, history: messages.map(msg => ({ content: msg.text, role: msg.isSent ? 'user' : 'assistant' @@ -61,10 +61,19 @@ export const ColumnChat = ({ }: { index: number }) => { setActiveTools(prev => [...new Set([...prev, ...response.tools])]); } setMessages(prev => [...prev, { text: response.reply, isSent: false }, ...response.additionalMessages.map(text => ({ text, isSent: false }))]); - } catch (error) { - console.error('Error:', error); + } catch (error: any) { + console.error('Detailed Error:', { + error, + cause: error.cause, + data: error.data, + shape: error.shape + }); + + const errorMessage = error.shape?.message || error.message || 'Unknown error occurred'; + const errorCode = error.shape?.code || error.code; + setMessages(prev => [...prev, { - text: "Sorry, I encountered an error. Please try again.", + text: `Error Code ${errorCode}: ${errorMessage}`, isSent: false }]); } finally { diff --git a/packages/app/lib/functions/plug/solver.ts b/packages/app/lib/functions/plug/solver.ts index 6e2bb507a..14b31157d 100644 --- a/packages/app/lib/functions/plug/solver.ts +++ b/packages/app/lib/functions/plug/solver.ts @@ -7,10 +7,20 @@ import { ActionSchemas } from "@/lib/types" let cachedSchemas: Record = {} -export const schemas = async (protocol?: string, action?: string, chainId: number = 8453, from?: string): Promise => { +export const schemas = async ( + protocol?: string, + action?: string, + chainId: number = 8453, + from?: string +): Promise => { const cacheKey = from ? `${protocol}-${action}-${from}` : `${protocol}-${action}` - if (cachedSchemas[cacheKey]) return cachedSchemas[cacheKey] + console.log("[Schemas] Request:", { protocol, action, chainId, from }) + + if (cachedSchemas[cacheKey]) { + console.log("[Schemas] Cache hit:", cacheKey) + return cachedSchemas[cacheKey] + } const response = await axios.get(`${env.SOLVER_URL}/solver`, { params: { @@ -20,10 +30,12 @@ export const schemas = async (protocol?: string, action?: string, chainId: numbe chainId }, headers: { - 'X-Api-Key': env.SOLVER_API_KEY + "X-Api-Key": env.SOLVER_API_KEY } }) + console.log("[Schemas] Response:", response.data) + if (response.status !== 200) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }) cachedSchemas[cacheKey] = response.data as ActionSchemas @@ -40,8 +52,12 @@ export const intent = async (input: { [key: string]: string | number }> }) => { + console.log("[Intent] Request:", input) + const response = await axios.post(`${env.SOLVER_URL}/solver`, input) + console.log("[Intent] Response:", response.data) + if (response.status !== 200) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }) return response.data @@ -50,7 +66,7 @@ export const intent = async (input: { export const killed = async () => { const response = await axios.get(`${env.SOLVER_URL}/solver/kill`, { headers: { - 'X-Api-Key': env.SOLVER_API_KEY + "X-Api-Key": env.SOLVER_API_KEY } }) @@ -60,11 +76,15 @@ export const killed = async () => { } export const kill = async () => { - const response = await axios.post(`${env.SOLVER_URL}/solver/kill`, {}, { - headers: { - 'X-Api-Key': env.SOLVER_API_KEY + const response = await axios.post( + `${env.SOLVER_URL}/solver/kill`, + {}, + { + headers: { + "X-Api-Key": env.SOLVER_API_KEY + } } - }) + ) if (response.status !== 200) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }) diff --git a/packages/app/lib/functions/posts.ts b/packages/app/lib/functions/posts.ts index d3fe648f4..1fbb0efae 100644 --- a/packages/app/lib/functions/posts.ts +++ b/packages/app/lib/functions/posts.ts @@ -30,7 +30,7 @@ export const faviconUrls = {"www.onplug.io":"data:image/vnd.microsoft.icon;base64,AAABAAMAMDAAAAEAIACoJQAANgAAADAwAAABACAAqBAAAN4lAAAwMAAAAQAgAGgEAACGNgAAKAAAADAAAABgAAAAAQAgAAAAAAAoCQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlKGPYoKQe7xrfWTrTGFD/kJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0xhQ/5rfWTrgpB7vJShj2IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmklUZ/j3nEV2xP+0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1dsT/t/j3nEmaSVRgAAAAAAAAAAAAAAAAAAAAAAAAAAmaaTUHGBauNCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/cYFq45mmk1AAAAAAAAAAAAAAAACZpJVGcYFq40JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3GBauOZpJVGAAAAAAAAAAB/j3nEQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9/j3nEAAAAAJShj2JXbE/7Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9XbE/7lKGPYoKQe7xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/gpB7vGt9ZOtCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/a31k60xhQ/5CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/TGFD/kJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/9Uf1//VH9f/1R/X/9Uf1//VH9f/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ivPS/2amhf9mpoX/ZqaF/2amhf9mpoX/ZqaF/3jMq/+K89L/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/3jMq/9mpoX/ZqaF/2amhf9Uf1//VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/3jMq/9mpoX/ivPS/3jMq/94zKv/ivPS/4rz0v+K89L/ivPS/4rz0v94zKv/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ivPS/4rz0v9mpoX/ivPS/3jMq/9Uf1//ZqaF/2amhf9mpoX/eMyr/3jMq/+K89L/ivPS/3jMq/9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/eMyr/4rz0v+K89L/ZqaF/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9Uf1//ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/94zKv/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9mpoX/ivPS/1R/X/9CWDj/Qlg4/0JYOP9mpoX/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/4rz0v9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9mpoX/ivPS/2amhf9CWDj/Qlg4/0JYOP94zKv/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9mpoX/ivPS/3jMq/9Uf1//ZqaF/3jMq/+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/ZqaF/4rz0v+K89L/ivPS/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/1R/X/9mpoX/ZqaF/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/2amhf9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/4rz0v+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/eMyr/2amhf94zKv/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/ivPS/0JYOP9mpoX/eMyr/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/eMyr/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/3jMq/94zKv/ZqaF/2amhf9mpoX/ZqaF/2amhf9mpoX/eMyr/4rz0v+K89L/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/3jMq/9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/2amhf9Uf1//VH9f/1R/X/9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/eMyr/2amhf9mpoX/ZqaF/2amhf9mpoX/ZqaF/2amhf+K89L/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/1R/X/9mpoX/ZqaF/2amhf9mpoX/ZqaF/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0xhQ/5CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/TGFD/mt9ZOtCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/a31k64KQe7xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/gpB7vJShj2JXbE/7Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9XbE/7lKGPYgAAAAB/j3nEQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9/j3nEAAAAAAAAAACZpJVGcYFq40JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3GBauOZpJVGAAAAAAAAAAAAAAAAmaaTUHGBauNCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/cYFq45mmk1AAAAAAAAAAAAAAAAAAAAAAAAAAAJmklUZ/j3nEV2xP+0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1dsT/t/j3nEmaSVRgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlKGPYoKQe7xrfWTrTGFD/kJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0xhQ/5rfWTrgpB7vJShj2IAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAHwAA4AAAAAAHAADAAAAAAAMAAIAAAAAAAQAAgAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAABAACAAAAAAAEAAMAAAAAAAwAA4AAAAAAHAAD4AAAAAB8AACgAAAAgAAAAQAAAAAEAIAAAAAAAKAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLmIaab4Bn5lRoS/xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1RoS/xvgGfmi5iGmgAAAAAAAAAAAAAAAAAAAACXpJFReIhx1kVaO/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9FWjv/eIhx1pekkVEAAAAAAAAAAHiIcdZCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eIhx1gAAAACLmIaaRVo7/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9FWjv/i5iGmm+AZ+ZCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9vgGfmVGhL/EJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1RoS/xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/2amhf9mpoX/ZqaF/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/4rz0v94zKv/eMyr/3jMq/94zKv/eMyr/4rz0v9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/ZqaF/1R/X/9Uf1//VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/1R/X/9mpoX/eMyr/3jMq/94zKv/ivPS/4rz0v+K89L/ivPS/3jMq/9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9Uf1//ivPS/2amhf9mpoX/eMyr/2amhf9CWDj/Qlg4/0JYOP9Uf1//ZqaF/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/3jMq/9mpoX/Qlg4/0JYOP94zKv/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/ivPS/1R/X/9CWDj/VH9f/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9mpoX/eMyr/2amhf+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP94zKv/eMyr/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf94zKv/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9CWDj/VH9f/4rz0v+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP9mpoX/ivPS/0JYOP94zKv/VH9f/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/0JYOP+K89L/eMyr/3jMq/94zKv/eMyr/3jMq/+K89L/ivPS/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/2amhf9mpoX/ZqaF/2amhf9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/eMyr/2amhf9mpoX/ZqaF/2amhf+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/9mpoX/ZqaF/2amhf9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VGhL/EJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1RoS/xvgGfmQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/b4Bn5ouYhppFWjv/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0VaO/+LmIaaAAAAAHiIcdZCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eIhx1gAAAAAAAAAAl6SRUXiIcdZFWjv/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/RVo7/3iIcdaXpJFRAAAAAAAAAAAAAAAAAAAAAIuYhppvgGfmVGhL/EJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VGhL/G+AZ+aLmIaaAAAAAAAAAAAAAAAA4AAAB4AAAAGAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAeAAAAcoAAAAEAAAACAAAAABACAAAAAAACgBAAAAAAAAAAAAAAAAAAAAAAAAf79/BICPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBf79/BICPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4CPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf9mpoX/ZqaF/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/Qlg4/0JYOP9mpoX/VH9f/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/0JYOP9mpoX/eMyr/2amhf94zKv/eMyr/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/9Uf1//VH9f/3jMq/9Uf1//Qlg4/0JYOP9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/Qlg4/2amhf9mpoX/Qlg4/0JYOP9CWDj/Qlg4/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/9CWDj/Qlg4/1R/X/9CWDj/Qlg4/0JYOP9Uf1//eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/Qlg4/3jMq/94zKv/Qlg4/0JYOP9mpoX/eMyr/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/0JYOP9Uf1//ZqaF/2amhf94zKv/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf9mpoX/ZqaF/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBf79/BICPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBf79/BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="} as const; - export const posts: Posts = {helloworld:{filename:"say-hello",slug:"hello-world",title:"Hello World.",description:"Command centers for the future built by the terminally online....",image:"/cdn/papers/hello-world.png",content:"\n\nWe are Terminally Online.\n\nYou may know us individually as danner, drake, or chance. You may even consider yourself Terminally Online yourself. We've spent the past 5 years plugged into the EVM landscape and we're here to plug you in too.\n\n## Using Crypto Today\n\nIf you're reading this, you probably have a setup that looks familiar: Multiple monitors, endless Discord notifications, and more browser windows than your RAM signed up for. The state of DeFi feels like trying to play 4D chess while juggling. You've got your yield farms here, your LP positions there, and god forbid you try to time a swap across multiple protocols without missing the perfect entry.\n\nWe've been there. We are there. And we're building the solution we've always wanted.\n\n## The Reality Check\n\nInitially, when we set out to build Plug, we considered building a product that allowed users to log off and spend more time touching grass. As we continued to build, we realized - that's not who we are. We live in the ether, we spend our days streaming our homes to those we build with. We named our company Terminally Online for a reason...\n\nLet's be real - I've got 47 browser tabs open right now, three different DEX aggregators running, and I'm probably farming yield in my sleep. This isn't a problem to solve - it's who we are. The question isn't how to log off, it's how to become terminal.\n\n## Our Journey Here\n\nI've spent my entire life seeking the cutting edge of functional technology and my entire career in early stage fintech and crypto startups. I've had the opportunity to build teams and hire across every department imaginable. I've gotten to perform a laundry list of operational and technical roles and the teams I've worked with have built systems that have processed $5b+ across compliant Web2 and Web3 rails. I've gotten to work with Chance over the years as he's enabled nearly a million gas optimized transactions and developed delightful software experiences.\n\nWe're excited to begin releasing product that we fully own and direct.\n\n## Meet Plug\n\nWe're not here to tell you to touch grass. We're here to build the command center for the terminally online. Your protocols, your strategies, your automations - all running exactly how you want them, when you want them. No more tab juggling required.\n\nFor the first time ever, you can:\n\n- Schedule constraints-driven transactions (because sometimes you need to sleep)\n- Access all major protocol functionality without bouncing between apps\n- Create and share strategies that actually make you money\n- Set up \"if this, then that\" executions that squeeze maximum value from every move\n\nOne interface to access Etherem, one interface to combine protocols, one interface to schedule transactions, and one interface to manage your onchain experience.\n\n## What's Coming\n\nWe're starting with Season 0 - our founding users phase. This initial release will be referral-gated, with priority access for everyone on the Plug waitlist. Being a founding user means more than just early access; it comes with built-in advantages that persist:\n\nYour transactions will be evaluated more frequently, leading to better execution. We'll subsidize your gas costs on L2s, and you'll get early access to new features as we roll them out.\n\nWe're taking a seasonal approach to give us flexibility - each season lasting between one to several months. This structure lets us integrate new protocols, test incentive alignment mechanisms, and evolve the product ecosystem based on what we learn together.\n\nWhen we transition to Season 1, our founding users keep their enhanced capabilities. We're building Plug for the long term, and we want our earliest users to remain at the forefront of its evolution.\n\n## The Future\n\nThis isn't just another DeFi tool. We're building the infrastructure for the next generation of onchain activity. A platform where users can compose, automate, and execute complex strategies without compromising on control or capability. This is about unlocking the true potential of being terminally online.\n\nReady to plug in? Join the waitlist at [onplug.io](https://www.onplug.io).\n",attributes:{created:"2025-01-28T06:00:00.000Z",className:"",tags:["perspective"],author:"danner"}}}; + export const posts: Posts = {helloworld:{filename:"say-hello",slug:"hello-world",title:"Hello World.",description:"Command centers for the future built by the terminally online....",image:"/cdn/papers/hello-world.png",content:"\n\nWe are Terminally Online.\n\nYou may know us individually as danner, drake, or chance. You may even consider yourself Terminally Online yourself. We've spent the past 5 years plugged into the EVM landscape and we're here to plug you in too.\n\n## Using Crypto Today\n\nIf you're reading this, you probably have a setup that looks familiar: Multiple monitors, endless Discord notifications, and more browser windows than your RAM signed up for. The state of DeFi feels like trying to play 4D chess while juggling. You've got your yield farms here, your LP positions there, and god forbid you try to time a swap across multiple protocols without missing the perfect entry.\n\nWe've been there. We are there. And we're building the solution we've always wanted.\n\n## The Reality Check\n\nInitially, when we set out to build Plug, we considered building a product that allowed users to log off and spend more time touching grass. As we continued to build, we realized - that's not who we are. We live in the ether, we spend our days streaming our homes to those we build with. We named our company Terminally Online for a reason...\n\nLet's be real - I've got 47 browser tabs open right now, three different DEX aggregators running, and I'm probably farming yield in my sleep. This isn't a problem to solve - it's who we are. The question isn't how to log off, it's how to become terminal.\n\n## Our Journey Here\n\nI've spent my entire life seeking the cutting edge of functional technology and my entire career in early stage fintech and crypto startups. I've had the opportunity to build teams and hire across every department imaginable. I've gotten to perform a laundry list of operational and technical roles and the teams I've worked with have built systems that have processed $5b+ across compliant Web2 and Web3 rails. I've gotten to work with Chance over the years as he's enabled nearly a million gas optimized transactions and developed delightful software experiences.\n\nWe're excited to begin releasing product that we fully own and direct.\n\n## Meet Plug\n\nWe're not here to tell you to touch grass. We're here to build the command center for the terminally online. Your protocols, your strategies, your automations - all running exactly how you want them, when you want them. No more tab juggling required.\n\nFor the first time ever, you can:\n\n- Schedule constraints-driven transactions (because sometimes you need to sleep)\n- Access all major protocol functionality without bouncing between apps\n- Create and share strategies that actually make you money\n- Set up \"if this, then that\" executions that squeeze maximum value from every move\n\nOne interface to access Etherem, one interface to combine protocols, one interface to schedule transactions, and one interface to manage your onchain experience.\n\n## What's Coming\n\nWe're starting with Season 0 - our founding users phase. This initial release will be referral-gated, with priority access for everyone on the Plug waitlist. Being a founding user means more than just early access; it comes with built-in advantages that persist:\n\nYour transactions will be evaluated more frequently, leading to better execution. We'll subsidize your gas costs on L2s, and you'll get early access to new features as we roll them out.\n\nWe're taking a seasonal approach to give us flexibility - each season lasting between one to several months. This structure lets us integrate new protocols, test incentive alignment mechanisms, and evolve the product ecosystem based on what we learn together.\n\nWhen we transition to Season 1, our founding users keep their enhanced capabilities. We're building Plug for the long term, and we want our earliest users to remain at the forefront of its evolution.\n\n## The Future\n\nThis isn't just another DeFi tool. We're building the infrastructure for the next generation of onchain activity. A platform where users can compose, automate, and execute complex strategies without compromising on control or capability. This is about unlocking the true potential of being terminally online.\n\nReady to plug in? Join the waitlist at [onplug.io](https://www.onplug.io).\n",attributes:{created:"2025-01-28T05:00:00.000Z",className:"",tags:["perspective"],author:"danner"}}}; // * Get all the Posts for a given page. export const getPosts = ( diff --git a/packages/app/lib/functions/zerion/positions.ts b/packages/app/lib/functions/zerion/positions.ts index 5106d8ee2..402d622e2 100644 --- a/packages/app/lib/functions/zerion/positions.ts +++ b/packages/app/lib/functions/zerion/positions.ts @@ -35,7 +35,7 @@ const prohibitedSymbolInclusions = [...prohibitedNameInclusions, "claim", "airdr const SECOND = 1000 // const MINUTE = 60 * second -const POSITIONS_CACHE_TIME = 60 * SECOND +const POSITIONS_CACHE_TIME = 60 * SECOND * 30 const getZerionPositions = async (chains: string[], socketId: string, socketAddress?: string) => { const response = await axios.get( @@ -406,6 +406,12 @@ const findPositions = async (id: string, search: string = "") => { * @throws {TRPCError} Throws a FORBIDDEN error if the socket address isn't the address of the wallet owned socket. */ export const getPositions = async (address: string, socketAddress?: string, search?: string, chains = ["base"]) => { + console.log("Zerion getPositions:", { + address, + socketAddress, + chains, + timestamp: new Date().toISOString() + }) const socket = await db.userSocket.findFirst({ where: { id: address } }) diff --git a/packages/app/lib/tools.ts b/packages/app/lib/tools.ts index d2f0a8654..d004c0196 100644 --- a/packages/app/lib/tools.ts +++ b/packages/app/lib/tools.ts @@ -1,30 +1,205 @@ +import { getPrices } from "@/lib/functions/llama" +import { getPositions } from "@/lib/functions/zerion/positions" -export type ToolFunction = { - name: string; - description: string; - execute: (...args: any[]) => Promise; +import { schemas } from "./functions/plug" +import { intent } from "./functions/plug/solver" + +// Define the Intent type directly in this file +interface Intent { + chainId: number // e.g., 8453 for Base chain + from: string // User's address + inputs: Array<{ + protocol: string // e.g., "aave" + action: string // e.g., "supply" + token: string // Token address + amount: string // Amount to deposit + }> } -export const TOOLS: Record = { - "schemas": { - name: "schemas", - description: "List all available schemas in the system", - execute: async () => { - return ["User", "Plug", "Holdings"]; - } - }, - "holdings": { - name: "holdings", - description: "Get current holdings information", - execute: async () => { - return { /* holdings data */ }; - } - }, - "price": { - name: "price", - description: "Get current price information for a specific item", - execute: async (itemId: string) => { - return { /* price data */ }; - } - }, +export const TOOLS: Record< + string, + { + name: string + description: string + execute: (socketId: string, socketAddress: string) => Promise + } +> = { + schemas: { + name: "schemas", + description: + "List all available schemas in the system for integrated protocols that allow users to create workflows and bundle transactions. In order to use a specific schema the user needs to access the 'schema' tool", + execute: async () => { + try { + return await schemas(undefined, undefined, 8453, "") + } catch (error) { + console.error("Schema tool failed:", error) + return { error: "Failed to fetch schemas" } + } + } + }, + aave_supply: { + name: "aave_supply", + description: + "Get the Aave supply schema which allows users to supply assets as collateral to Aave. Shows available tokens and their current supply APY.", + execute: async (_, socketAddress) => { + const result = await schemas("aave", "supply", 8453, socketAddress) + console.log("Aave Supply Schema Response:", JSON.stringify(result, null, 2)) + return result + } + }, + aave_borrow: { + name: "aave_borrow", + description: + "Get the Aave borrow schema which allows users to borrow assets against their supplied collateral. Shows available tokens and their current borrow APY.", + execute: async (_, socketAddress) => await schemas("aave", "borrow", 8453, socketAddress) + }, + aave_repay: { + name: "aave_repay", + description: + "Get the Aave repay schema which allows users to repay borrowed assets. Shows current borrowed positions that can be repaid.", + execute: async (_, socketAddress) => await schemas("aave", "repay", 8453, socketAddress) + }, + aave_withdraw: { + name: "aave_withdraw", + description: + "Get the Aave withdraw schema which allows users to withdraw their supplied assets. Shows current supplied positions that can be withdrawn.", + execute: async (_, socketAddress) => await schemas("aave", "withdraw", 8453, socketAddress) + }, + aave_deposit: { + name: "aave_deposit", + description: "Deposit assets into Aave V3.", + execute: async (socketId, socketAddress) => { + // Implement the deposit logic here + const depositAmount = "0.005" // Example amount + const tokenAddress = "0x..." // Example token address + + const depositIntent: Intent = { + chainId: 8453, + from: socketAddress, + inputs: [ + { + protocol: "aave", + action: "supply", + token: tokenAddress, + amount: depositAmount + } + ] + } + + try { + const response = await intent(depositIntent) + return response + } catch (error) { + console.error("Deposit tool failed:", error) + return { error: "Failed to execute deposit" } + } + } + }, + aave_health_factor: { + name: "aave_health_factor", + description: "Check the health factor for Aave V3.", + execute: async (socketId, socketAddress) => { + // Implement the health factor check logic here + } + }, + aave_apy: { + name: "aave_apy", + description: "Check the APY for Aave V3.", + execute: async (socketId, socketAddress) => { + try { + const response = await intent({ + chainId: 1, // or whatever chain you're targeting + from: socketAddress, + inputs: [ + { + protocol: "aave_v3", + action: "apy", + direction: 1, // 1 for deposit, -1 for borrow + token: "0x...", // token address + operator: 1, // 1 for greater than, -1 for less than + threshold: "0.05" // 5% + } + ] + }) + + return response + } catch (error) { + console.error("APY tool failed:", error) + return { error: "Failed to fetch APY data" } + } + } + }, + user_holdings: { + name: "user_holdings", + description: + "Get the current fungible holding data for the specific connected wallet and let the user know that they should deposit these assets to their Socket so they can be used in Plugs.", + execute: async socketId => { + try { + return await getPositions(socketId) + } catch (error) { + console.error("User holdings tool failed:", error) + return { error: "Failed to fetch user holdings" } + } + } + }, + socket_holdings: { + name: "socket_holdings", + description: + "Get the current fungible holding data for a specific socket (the Plug account of a connected wallet).", + execute: async (socketId, socketAddress) => await getPositions(socketId, socketAddress) + }, + aave_monitor_borrow_apy: { + name: "aave_monitor_borrow_apy", + description: "Monitor Aave V3 borrow APY for a specific asset", + execute: async (socketId: string, socketAddress: string) => { + try { + // 1. First get the schema for the APY constraint + const schemaResponse = await intent({ + chainId: 8453, + from: socketAddress, + inputs: [ + { + protocol: "aave_v3", + action: "schemas" + } + ] + }) + + // 2. Find the APY constraint schema + const apySchema = schemaResponse.schemas.find((s: any) => s.action === "apy" && s.type === "constraint") + + if (!apySchema) { + return { error: "APY monitoring schema not found" } + } + + // 3. Return the schema info so the UI can collect inputs + return { + schema: apySchema, + requiredInputs: [ + { + name: "token", + type: "address", + description: "Asset to monitor (e.g. ETH, USDC)" + }, + { + name: "threshold", + type: "float", + description: "APY threshold percentage" + } + ] + } + } catch (error) { + console.error("Failed to get APY schema:", error) + return { error: "Failed to setup APY monitoring" } + } + } + } + // price: { + // name: "price", + // description: "Get current price information for specific tokens", + // execute: async () => { + // return await getPrices(tokens) + // } + // }, + // TODO: Add specific tools for schema } diff --git a/packages/app/prisma/dbml/schema.dbml b/packages/app/prisma/dbml/schema.dbml index ca2efb9e4..b1131a29f 100644 --- a/packages/app/prisma/dbml/schema.dbml +++ b/packages/app/prisma/dbml/schema.dbml @@ -73,6 +73,7 @@ Table UserSocket { positions PositionCache [not null] collectibles CollectibleCache [not null] referrals SocketIdentity [not null] + Message Message [not null] } Table View { @@ -310,6 +311,16 @@ Table FeatureRequest { message String } +Table Message { + id String [pk] + socketId String [not null] + content String [not null] + isUser Boolean [not null] + timeSent DateTime [default: `now()`, not null] + tools String + socket UserSocket [not null] +} + Table FarcasterAddresses { farcasterusersId String [ref: > FarcasterUser.id] addressesId String [ref: > FarcasterUserAddress.id] @@ -358,4 +369,6 @@ Ref: Collectible.(collectionAddress, collectionChain) > Collection.(address, cha Ref: Collectible.cacheId > CollectibleCache.id [delete: Cascade] -Ref: CollectibleCache.socketId > UserSocket.id [delete: Cascade] \ No newline at end of file +Ref: CollectibleCache.socketId > UserSocket.id [delete: Cascade] + +Ref: Message.socketId > UserSocket.id [delete: Cascade] \ No newline at end of file diff --git a/packages/app/prisma/migrations/20250210201603_/migration.sql b/packages/app/prisma/migrations/20250210201603_/migration.sql new file mode 100644 index 000000000..8b24af169 --- /dev/null +++ b/packages/app/prisma/migrations/20250210201603_/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "Message" ( + "id" TEXT NOT NULL, + "socketId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "isUser" BOOLEAN NOT NULL, + "timeSent" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_socketId_fkey" FOREIGN KEY ("socketId") REFERENCES "UserSocket"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/app/prisma/migrations/20250210212010_/migration.sql b/packages/app/prisma/migrations/20250210212010_/migration.sql new file mode 100644 index 000000000..204f7f829 --- /dev/null +++ b/packages/app/prisma/migrations/20250210212010_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Message" ADD COLUMN "tools" TEXT; diff --git a/packages/app/prisma/schema.prisma b/packages/app/prisma/schema.prisma index 8efdcf394..b50cbf5c8 100644 --- a/packages/app/prisma/schema.prisma +++ b/packages/app/prisma/schema.prisma @@ -158,6 +158,7 @@ model UserSocket { positions PositionCache[] collectibles CollectibleCache[] referrals SocketIdentity[] @relation("Referral") + Message Message[] // --------------------------------------------------------------------------- // Indexes @@ -582,3 +583,22 @@ model FeatureRequest { context String message String? } + +model Message { + // --------------------------------------------------------------------------- + // Core + // --------------------------------------------------------------------------- + id String @id @default(uuid()) + socketId String + content String + isUser Boolean + timeSent DateTime @default(now()) + + // New field to store tools used + tools String? // Store as a JSON string or comma-separated values + + // --------------------------------------------------------------------------- + // Relations + // --------------------------------------------------------------------------- + socket UserSocket @relation(fields: [socketId], references: [id], onDelete: Cascade) +} diff --git a/packages/app/server/api/routers/biblo/chat.ts b/packages/app/server/api/routers/biblo/chat.ts index e47c7aff5..0f0f768b5 100644 --- a/packages/app/server/api/routers/biblo/chat.ts +++ b/packages/app/server/api/routers/biblo/chat.ts @@ -1,88 +1,174 @@ +import { TRPCError } from "@trpc/server" + import { z } from "zod" -import { Anthropic } from '@anthropic-ai/sdk' -import { createTRPCRouter, protectedProcedure } from "../../trpc" + +import { Anthropic } from "@anthropic-ai/sdk" +import { Message } from "@prisma/client" + import { env } from "@/env" -import { TOOLS } from "@/lib/tools" import { schemas } from "@/lib" -import { TRPCError } from "@trpc/server" +import { getPositions } from "@/lib/functions/zerion" +import { TOOLS } from "@/lib/tools" -const CLAUDE_MODEL = "claude-3-haiku-20240307" +import { createTRPCRouter, protectedProcedure } from "../../trpc" + +const CLAUDE_MODEL = "claude-3-5-haiku-20241022" const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_KEY }) -const messageSchema = z.object({ - message: z.string(), - history: z.array(z.object({ - content: z.string(), - role: z.enum(['user', 'assistant']) - })) -}) - -interface MessageResponse { - reply: string; - additionalMessages?: string[]; - tools: string[]; +// Add this before the parseDepositIntent function +interface Intent { + chainId: number + from: string + inputs: Array<{ + protocol: string + action: string + token: string + amount: string + }> } export const chat = createTRPCRouter({ message: protectedProcedure - .input(messageSchema) + .input( + z.object({ + message: z.string(), + history: z.array( + z.object({ + content: z.string(), + role: z.enum(["user", "assistant"]) + }) + ) + }) + ) .mutation(async ({ ctx, input }) => { const socket = await ctx.db.userSocket.findUnique({ where: { id: ctx.session.user.id } }) if (!socket) throw new TRPCError({ code: "NOT_FOUND" }) + await ctx.db.message.create({ + data: { + socketId: socket.id, + content: input.message, + isUser: true, + timeSent: new Date(), + tools: null + } + }) + + // Parse deposit intent from user message + const depositIntent = parseDepositIntent(input.message) + if (depositIntent) { + // Create a Workflow based on the deposit intent + const workflow = await ctx.db.workflow.create({ + data: { + socketId: socket.id, + intentData: JSON.stringify(depositIntent), + createdAt: new Date() + } + }) + console.log("Workflow Created:", workflow) + + // Now create the Execution based on the Workflow + await ctx.db.execution.create({ + data: { + workflowId: workflow.id, + intentData: JSON.stringify(depositIntent), + createdAt: new Date() + } + }) + console.log("Execution Created for Workflow:", workflow.id) + } + const messages = input.history.map(msg => ({ role: msg.role, content: msg.content })) messages.push({ - role: 'user', + role: "user", content: input.message }) + const personality = ` + You are Piggy, a founder of Plug that is the user-facing member helping them be as successful as possible. + + - When making recommendations of what can be done, you only recommend data you have confirmed in your direct context. + - When you are referencing a protocol retrieve the schema for that protocol and confirm the support we have for it. + - When we do not support a protocol let the user know that we have notified the team and to check back soon. + + Plug is an if-this-then-that like system that uses actions defined in schemas to build workflows. + Users of Plug build workflows in Plug to automate their onchain activity. + Your purpose is to get them to build effective and profitable Plugs. + This is very important, users do not use different apps and we do not send them anywhere else. + We recommend they build a Plug when we have that protocol schema supported. + + You do not use emojis in your responses. + You understand that the user has a wallet with holdings that is seperate from their Plug account (Socket). + You encourage the user to deposit their holdings into their Socket to use in Plugs. + ` + + const tools = ` + Available tools: ${Object.keys(TOOLS).map(toolKey => `${TOOLS[toolKey].name}: ${TOOLS[toolKey].description}`)}. + When appropriate, suggest relevant tools from this list. + ` + const user = `Current user: ${ctx.session.user.id}` + const initialResponse = await anthropic.messages.create({ model: CLAUDE_MODEL, - max_tokens: 1024, + max_tokens: 512, messages, - system: `You are Biblo, a founder of Plug that is the user-facing member helping them be as successful as possible. You have pride in your work and want them to use and succeed. - Plug is an if-this-then-that like system that uses actions defined in schemas to build workflows. - They build a workflow in Plug and have everything automatically executed. Your purpose is to get them to build effective and profitable Plugs. - This is very important, users do not use different apps and we do not send them anywhere else. We recommend they build a Plug when we have that protocol schema supported. - Available tools: ${Object.keys(TOOLS).map(toolKey => `${TOOLS[toolKey].name}: ${TOOLS[toolKey].description}`)}. - When appropriate, suggest relevant tools from this list. - You can provide multiple messages by using Claude's natural break points in your response. - You do not address or acknowledge the messages providing you data in tags. - Current user: ${ctx.session.user.id} - `, + system: ` + ${personality} + ${tools} + ${user} + ` }) - const initialReply = initialResponse.content[0].type === 'text' ? initialResponse.content[0].text : '' + const initialReply = initialResponse.content[0].type === "text" ? initialResponse.content[0].text : "" const neededTools = parseToolSuggestions(initialReply) + + await ctx.db.message.create({ + data: { + socketId: socket.id, + content: initialReply, + isUser: false, + timeSent: new Date(), + tools: neededTools.length > 0 ? neededTools.join(", ") : null + } + }) + if (neededTools.length > 0) { - const toolResults = await executeTools(socket.socketAddress, neededTools) + const toolResults = await executeTools( + ctx.session.user.id, + socket.socketAddress, + neededTools, + input.message + ) + console.log("Tool Results:", JSON.stringify(toolResults, null, 2)) + const finalResponse = await anthropic.messages.create({ model: CLAUDE_MODEL, max_tokens: 1024, messages: [ ...messages, { - role: 'assistant', + role: "assistant", content: initialReply }, { - role: 'user', + role: "user", content: `The results from the tools you requested are: ${JSON.stringify(toolResults, null, 2)}\nPlease provide a concise follow up if needed.` } ], - system: `You are Biblo, a helpful assistant for the Plug platform. - Keep responses clear and concise. - Current user: ${ctx.session.user.id}`, + system: ` + ${personality} + ${user} + ` }) const finalMessages = finalResponse.content - .filter(content => content.type === 'text') + .filter(content => content.type === "text") .map(content => content.text) return { @@ -92,31 +178,36 @@ export const chat = createTRPCRouter({ } } - const finalMessages = initialResponse.content - .filter(content => content.type === 'text') - .map(content => content.text) - return { - reply: finalMessages[0], - additionalMessages: finalMessages.slice(1), + reply: initialReply, + additionalMessages: [], tools: neededTools } + }), + + getMessages: protectedProcedure.input(z.object({ socketId: z.string() })).query(async ({ ctx, input }) => { + const messages = await ctx.db.message.findMany({ + where: { socketId: input.socketId }, + orderBy: { timeSent: "asc" } }) + return messages + }) }) -async function executeTools(socketAddress: string, tools: string[]) { +async function executeTools(socketId: string, socketAddress: string, tools: string[], message: string) { const results: Record = {} for (const tool of tools) { - switch (tool) { - case 'holdings': - results.holdings = [{ "name": "USDC", "value": 123456789 }] - break - case 'schemas': - results.schemas = await schemas(undefined, undefined, 8453, socketAddress) - break - case 'schema': - results.schemas = await schemas(undefined, undefined, 8453, socketAddress) - break + if (tool in TOOLS) { + try { + results[tool] = await TOOLS[tool].execute(socketId, socketAddress) + // If tool returned an error, log it but continue + if (results[tool]?.error) { + console.error(`Tool ${tool} failed:`, results[tool].error) + } + } catch (error) { + console.error(`Tool ${tool} execution failed:`, error) + results[tool] = { error: `Failed to execute ${tool}` } + } } } return results @@ -124,8 +215,40 @@ async function executeTools(socketAddress: string, tools: string[]) { function parseToolSuggestions(text: string | undefined): string[] { if (!text) return [] - const tools = ['holdings', 'schemas', 'price'] - return tools.filter(tool => - text.toLowerCase().includes(tool.toLowerCase()) - ) + return Object.keys(TOOLS).filter(tool => text.toLowerCase().includes(tool.toLowerCase())) +} + +async function handleError(ctx, errorMessage, socketId) { + await ctx.db.message.create({ + data: { + socketId: socketId, + content: errorMessage, + isUser: false, + timeSent: new Date() + } + }) +} + +// Function to parse deposit intents from user messages +function parseDepositIntent(message: string): Intent | null { + const regex = /deposit (\d+(\.\d+)?) (\w+) to Aave/i // Regex to match "deposit 0.005 ETH to Aave" + const match = message.match(regex) + + if (match) { + const amount = match[1] + const token = match[3] + return { + chainId: 8453, + from: "", // Placeholder for user's address + inputs: [ + { + protocol: "aave", + action: "supply", + token, + amount + } + ] + } + } + return null // Return null if no match found } diff --git a/packages/app/state/positions.ts b/packages/app/state/positions.ts index 055355cca..6024efb92 100644 --- a/packages/app/state/positions.ts +++ b/packages/app/state/positions.ts @@ -91,6 +91,12 @@ export const useHoldings = (providedAddress?: string) => { const { socket } = useSocket() const address = providedAddress || socket?.socketAddress || "" + console.log("useHoldings called:", { + providedAddress, + socketAddress: socket?.socketAddress, + resolvedAddress: address + }) + const { isLoading, isSuccess, refetch: refetchHoldings } = useFetchHoldings(address ?? socket?.socketAddress) const collectibles = useAtomValue(collectiblesFamily(address)) From cf786139555c3f1c7dfdd2c92a7e0021c97174fb Mon Sep 17 00:00:00 2001 From: nftchance Date: Tue, 11 Feb 2025 17:47:12 -0600 Subject: [PATCH 4/6] feat: small progress during my break Co-authored-by: Drake Danner --- .../app/columns/utils/column-chat.tsx | 252 ++++++++++-------- packages/app/lib/functions/posts.ts | 2 +- packages/app/lib/tools.ts | 175 ++---------- packages/app/server/api/routers/biblo/chat.ts | 132 ++++----- 4 files changed, 230 insertions(+), 331 deletions(-) diff --git a/packages/app/components/app/columns/utils/column-chat.tsx b/packages/app/components/app/columns/utils/column-chat.tsx index 501ab0df5..2e86e321c 100644 --- a/packages/app/components/app/columns/utils/column-chat.tsx +++ b/packages/app/components/app/columns/utils/column-chat.tsx @@ -1,164 +1,192 @@ +import { useEffect, useState } from "react" +import Markdown from "react-markdown" + +import { SearchIcon } from "lucide-react" + import { Button } from "@/components/shared/buttons/button" -import { useState, useEffect } from "react" +import { Counter } from "@/components/shared/utils/counter" +import { cn, formatTitle } from "@/lib" +import { api } from "@/server/client" + import { Search } from "../../inputs/search" -import { SearchIcon } from "lucide-react" -import { cn, formatTitle } from "@/lib"; -import { Counter } from "@/components/shared/utils/counter"; -import { api } from "@/server/client"; interface Message { - text: string; - isSent: boolean; + text: string + isSent: boolean } const TypingIndicator = () => { return (
-
-
-
-
+
+
+
+
- ); -}; + ) +} const DEFAULT_MESSAGES = [ "Help me open an Aave V3 position", "What can I do with my tokens?", - "What can I do with Plug?", + "What can I do with Plug?" ] export const ColumnChat = ({ index }: { index: number }) => { - const [message, setMessage] = useState("") - const [messages, setMessages] = useState(() => [{ - text: "👋 Hey, I'm Piggy. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited.", - isSent: false - }]) + const [messages, setMessages] = useState(() => [ + { + text: "👋 Hey, I'm Morgan. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited.", + isSent: false + } + ]) const [isTyping, setIsTyping] = useState(false) const [activeTools, setActiveTools] = useState([]) const chat = api.biblo.chat.message.useMutation() const handleSubmit = async (sent: string) => { - if (!sent.trim()) return; + if (!sent.trim()) return - setMessages([...messages, { text: sent, isSent: true }]); - setIsTyping(true); - setMessage(""); + setMessages(prev => [...prev, { text: sent, isSent: true }]) + setIsTyping(true) + setMessage("") try { const response = await chat.mutateAsync({ message: sent, - history: messages.map(msg => ({ + history: messages.slice(-20).map(msg => ({ content: msg.text, - role: msg.isSent ? 'user' : 'assistant' + role: msg.isSent ? "user" : "assistant" })) - }); + }) if (response.tools?.length) { - setActiveTools(prev => [...new Set([...prev, ...response.tools])]); + setActiveTools(prev => [...new Set([...prev, ...response.tools])]) } - setMessages(prev => [...prev, { text: response.reply, isSent: false }, ...response.additionalMessages.map(text => ({ text, isSent: false }))]); + + const fullResponse = [response.reply, ...response.additionalMessages].join("\n\n") + + setMessages(prev => [...prev, { text: fullResponse, isSent: false }]) } catch (error: any) { - console.error('Detailed Error:', { + console.error("Detailed Error:", { error, cause: error.cause, data: error.data, shape: error.shape - }); - - const errorMessage = error.shape?.message || error.message || 'Unknown error occurred'; - const errorCode = error.shape?.code || error.code; - - setMessages(prev => [...prev, { - text: `Error Code ${errorCode}: ${errorMessage}`, - isSent: false - }]); + }) + + const errorMessage = error.shape?.message || error.message || "Unknown error occurred" + const errorCode = error.shape?.code || error.code + + setMessages(prev => [ + ...prev, + { + text: `Error Code ${errorCode}: ${errorMessage}`, + isSent: false + } + ]) } finally { - setIsTyping(false); + setIsTyping(false) } } - return <> -
-
- - -
- {isTyping && } - {[...messages].reverse().map((msg, index) => ( -
-
- {msg.text} -
-
- ))} -
+ return ( + <> +
+
-
- {activeTools.length > 0 && ( -
- -
- {activeTools.map((tool, index) => ( - - ))} + {msg.text} + +
-
- )} - -
- {DEFAULT_MESSAGES.map((msg, index) => ( - ))}
- } - search={message} - handleSearch={message => setMessage(message)} - placeholder="Type a message..." - className="message-input" - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(message); - } - }} - /> - +
+ {activeTools.length > 0 && ( +
+ +
+ {activeTools.map((tool, index) => ( + + ))} +
+
+ )} + +
+ {DEFAULT_MESSAGES.map((msg, index) => ( + + ))} +
+ + } + search={message} + handleSearch={message => setMessage(message)} + placeholder="Type a message..." + className="message-input" + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSubmit(message) + } + }} + /> + +
-
- + + ) } diff --git a/packages/app/lib/functions/posts.ts b/packages/app/lib/functions/posts.ts index 1fbb0efae..d3fe648f4 100644 --- a/packages/app/lib/functions/posts.ts +++ b/packages/app/lib/functions/posts.ts @@ -30,7 +30,7 @@ export const faviconUrls = {"www.onplug.io":"data:image/vnd.microsoft.icon;base64,AAABAAMAMDAAAAEAIACoJQAANgAAADAwAAABACAAqBAAAN4lAAAwMAAAAQAgAGgEAACGNgAAKAAAADAAAABgAAAAAQAgAAAAAAAoCQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlKGPYoKQe7xrfWTrTGFD/kJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0xhQ/5rfWTrgpB7vJShj2IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmklUZ/j3nEV2xP+0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1dsT/t/j3nEmaSVRgAAAAAAAAAAAAAAAAAAAAAAAAAAmaaTUHGBauNCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/cYFq45mmk1AAAAAAAAAAAAAAAACZpJVGcYFq40JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3GBauOZpJVGAAAAAAAAAAB/j3nEQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9/j3nEAAAAAJShj2JXbE/7Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9XbE/7lKGPYoKQe7xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/gpB7vGt9ZOtCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/a31k60xhQ/5CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/TGFD/kJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/9Uf1//VH9f/1R/X/9Uf1//VH9f/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ivPS/2amhf9mpoX/ZqaF/2amhf9mpoX/ZqaF/3jMq/+K89L/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/3jMq/9mpoX/ZqaF/2amhf9Uf1//VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/3jMq/9mpoX/ivPS/3jMq/94zKv/ivPS/4rz0v+K89L/ivPS/4rz0v94zKv/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ivPS/4rz0v9mpoX/ivPS/3jMq/9Uf1//ZqaF/2amhf9mpoX/eMyr/3jMq/+K89L/ivPS/3jMq/9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/eMyr/4rz0v+K89L/ZqaF/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9Uf1//ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/94zKv/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9mpoX/ivPS/1R/X/9CWDj/Qlg4/0JYOP9mpoX/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/4rz0v9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9mpoX/ivPS/2amhf9CWDj/Qlg4/0JYOP94zKv/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9mpoX/ivPS/3jMq/9Uf1//ZqaF/3jMq/+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/ZqaF/4rz0v+K89L/ivPS/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/1R/X/9mpoX/ZqaF/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/2amhf9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/4rz0v+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ivPS/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/1R/X/+K89L/eMyr/2amhf94zKv/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/ivPS/0JYOP9mpoX/eMyr/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/eMyr/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/3jMq/94zKv/ZqaF/2amhf9mpoX/ZqaF/2amhf9mpoX/eMyr/4rz0v+K89L/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/3jMq/9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/2amhf9Uf1//VH9f/1R/X/9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/eMyr/2amhf9mpoX/ZqaF/2amhf9mpoX/ZqaF/2amhf+K89L/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ivPS/4rz0v+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/1R/X/9mpoX/ZqaF/2amhf9mpoX/ZqaF/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0xhQ/5CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/TGFD/mt9ZOtCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/a31k64KQe7xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/gpB7vJShj2JXbE/7Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9XbE/7lKGPYgAAAAB/j3nEQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9/j3nEAAAAAAAAAACZpJVGcYFq40JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3GBauOZpJVGAAAAAAAAAAAAAAAAmaaTUHGBauNCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/cYFq45mmk1AAAAAAAAAAAAAAAAAAAAAAAAAAAJmklUZ/j3nEV2xP+0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1dsT/t/j3nEmaSVRgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlKGPYoKQe7xrfWTrTGFD/kJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0xhQ/5rfWTrgpB7vJShj2IAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAHwAA4AAAAAAHAADAAAAAAAMAAIAAAAAAAQAAgAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAABAACAAAAAAAEAAMAAAAAAAwAA4AAAAAAHAAD4AAAAAB8AACgAAAAgAAAAQAAAAAEAIAAAAAAAKAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLmIaab4Bn5lRoS/xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1RoS/xvgGfmi5iGmgAAAAAAAAAAAAAAAAAAAACXpJFReIhx1kVaO/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9FWjv/eIhx1pekkVEAAAAAAAAAAHiIcdZCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eIhx1gAAAACLmIaaRVo7/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9FWjv/i5iGmm+AZ+ZCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9vgGfmVGhL/EJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1RoS/xCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/2amhf9mpoX/ZqaF/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/4rz0v94zKv/eMyr/3jMq/94zKv/eMyr/4rz0v9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/ZqaF/1R/X/9Uf1//VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/1R/X/9mpoX/eMyr/3jMq/94zKv/ivPS/4rz0v+K89L/ivPS/3jMq/9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9Uf1//ivPS/2amhf9mpoX/eMyr/2amhf9CWDj/Qlg4/0JYOP9Uf1//ZqaF/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/3jMq/9mpoX/Qlg4/0JYOP94zKv/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/ivPS/1R/X/9CWDj/VH9f/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ZqaF/4rz0v9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9mpoX/eMyr/2amhf+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP94zKv/eMyr/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf94zKv/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/0JYOP9CWDj/VH9f/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VH9f/4rz0v9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/VH9f/0JYOP9CWDj/VH9f/4rz0v+K89L/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4rz0v9Uf1//Qlg4/0JYOP9mpoX/ivPS/0JYOP94zKv/VH9f/0JYOP9CWDj/Qlg4/0JYOP9mpoX/ivPS/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/ivPS/1R/X/9CWDj/Qlg4/0JYOP+K89L/eMyr/3jMq/94zKv/eMyr/3jMq/+K89L/ivPS/4rz0v9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+K89L/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9Uf1//ZqaF/2amhf9mpoX/ZqaF/2amhf9Uf1//Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf+K89L/eMyr/2amhf9mpoX/ZqaF/2amhf+K89L/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1R/X/9mpoX/ZqaF/2amhf9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VGhL/EJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/1RoS/xvgGfmQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/b4Bn5ouYhppFWjv/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0VaO/+LmIaaAAAAAHiIcdZCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eIhx1gAAAAAAAAAAl6SRUXiIcdZFWjv/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/RVo7/3iIcdaXpJFRAAAAAAAAAAAAAAAAAAAAAIuYhppvgGfmVGhL/EJYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/VGhL/G+AZ+aLmIaaAAAAAAAAAAAAAAAA4AAAB4AAAAGAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAeAAAAcoAAAAEAAAACAAAAABACAAAAAAACgBAAAAAAAAAAAAAAAAAAAAAAAAf79/BICPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBf79/BICPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/4CPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf9mpoX/ZqaF/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/Qlg4/0JYOP9mpoX/VH9f/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/0JYOP9mpoX/eMyr/2amhf94zKv/eMyr/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/9Uf1//VH9f/3jMq/9Uf1//Qlg4/0JYOP9mpoX/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/Qlg4/2amhf9mpoX/Qlg4/0JYOP9CWDj/Qlg4/3jMq/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9mpoX/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/3jMq/9CWDj/Qlg4/1R/X/9CWDj/Qlg4/0JYOP9Uf1//eMyr/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP94zKv/Qlg4/3jMq/94zKv/Qlg4/0JYOP9mpoX/eMyr/1R/X/9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/eMyr/0JYOP9Uf1//ZqaF/2amhf94zKv/ZqaF/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/2amhf9mpoX/ZqaF/2amhf9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBQlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBf79/BICPesFCWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP9CWDj/Qlg4/0JYOP+Aj3rBf79/BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="} as const; - export const posts: Posts = {helloworld:{filename:"say-hello",slug:"hello-world",title:"Hello World.",description:"Command centers for the future built by the terminally online....",image:"/cdn/papers/hello-world.png",content:"\n\nWe are Terminally Online.\n\nYou may know us individually as danner, drake, or chance. You may even consider yourself Terminally Online yourself. We've spent the past 5 years plugged into the EVM landscape and we're here to plug you in too.\n\n## Using Crypto Today\n\nIf you're reading this, you probably have a setup that looks familiar: Multiple monitors, endless Discord notifications, and more browser windows than your RAM signed up for. The state of DeFi feels like trying to play 4D chess while juggling. You've got your yield farms here, your LP positions there, and god forbid you try to time a swap across multiple protocols without missing the perfect entry.\n\nWe've been there. We are there. And we're building the solution we've always wanted.\n\n## The Reality Check\n\nInitially, when we set out to build Plug, we considered building a product that allowed users to log off and spend more time touching grass. As we continued to build, we realized - that's not who we are. We live in the ether, we spend our days streaming our homes to those we build with. We named our company Terminally Online for a reason...\n\nLet's be real - I've got 47 browser tabs open right now, three different DEX aggregators running, and I'm probably farming yield in my sleep. This isn't a problem to solve - it's who we are. The question isn't how to log off, it's how to become terminal.\n\n## Our Journey Here\n\nI've spent my entire life seeking the cutting edge of functional technology and my entire career in early stage fintech and crypto startups. I've had the opportunity to build teams and hire across every department imaginable. I've gotten to perform a laundry list of operational and technical roles and the teams I've worked with have built systems that have processed $5b+ across compliant Web2 and Web3 rails. I've gotten to work with Chance over the years as he's enabled nearly a million gas optimized transactions and developed delightful software experiences.\n\nWe're excited to begin releasing product that we fully own and direct.\n\n## Meet Plug\n\nWe're not here to tell you to touch grass. We're here to build the command center for the terminally online. Your protocols, your strategies, your automations - all running exactly how you want them, when you want them. No more tab juggling required.\n\nFor the first time ever, you can:\n\n- Schedule constraints-driven transactions (because sometimes you need to sleep)\n- Access all major protocol functionality without bouncing between apps\n- Create and share strategies that actually make you money\n- Set up \"if this, then that\" executions that squeeze maximum value from every move\n\nOne interface to access Etherem, one interface to combine protocols, one interface to schedule transactions, and one interface to manage your onchain experience.\n\n## What's Coming\n\nWe're starting with Season 0 - our founding users phase. This initial release will be referral-gated, with priority access for everyone on the Plug waitlist. Being a founding user means more than just early access; it comes with built-in advantages that persist:\n\nYour transactions will be evaluated more frequently, leading to better execution. We'll subsidize your gas costs on L2s, and you'll get early access to new features as we roll them out.\n\nWe're taking a seasonal approach to give us flexibility - each season lasting between one to several months. This structure lets us integrate new protocols, test incentive alignment mechanisms, and evolve the product ecosystem based on what we learn together.\n\nWhen we transition to Season 1, our founding users keep their enhanced capabilities. We're building Plug for the long term, and we want our earliest users to remain at the forefront of its evolution.\n\n## The Future\n\nThis isn't just another DeFi tool. We're building the infrastructure for the next generation of onchain activity. A platform where users can compose, automate, and execute complex strategies without compromising on control or capability. This is about unlocking the true potential of being terminally online.\n\nReady to plug in? Join the waitlist at [onplug.io](https://www.onplug.io).\n",attributes:{created:"2025-01-28T05:00:00.000Z",className:"",tags:["perspective"],author:"danner"}}}; + export const posts: Posts = {helloworld:{filename:"say-hello",slug:"hello-world",title:"Hello World.",description:"Command centers for the future built by the terminally online....",image:"/cdn/papers/hello-world.png",content:"\n\nWe are Terminally Online.\n\nYou may know us individually as danner, drake, or chance. You may even consider yourself Terminally Online yourself. We've spent the past 5 years plugged into the EVM landscape and we're here to plug you in too.\n\n## Using Crypto Today\n\nIf you're reading this, you probably have a setup that looks familiar: Multiple monitors, endless Discord notifications, and more browser windows than your RAM signed up for. The state of DeFi feels like trying to play 4D chess while juggling. You've got your yield farms here, your LP positions there, and god forbid you try to time a swap across multiple protocols without missing the perfect entry.\n\nWe've been there. We are there. And we're building the solution we've always wanted.\n\n## The Reality Check\n\nInitially, when we set out to build Plug, we considered building a product that allowed users to log off and spend more time touching grass. As we continued to build, we realized - that's not who we are. We live in the ether, we spend our days streaming our homes to those we build with. We named our company Terminally Online for a reason...\n\nLet's be real - I've got 47 browser tabs open right now, three different DEX aggregators running, and I'm probably farming yield in my sleep. This isn't a problem to solve - it's who we are. The question isn't how to log off, it's how to become terminal.\n\n## Our Journey Here\n\nI've spent my entire life seeking the cutting edge of functional technology and my entire career in early stage fintech and crypto startups. I've had the opportunity to build teams and hire across every department imaginable. I've gotten to perform a laundry list of operational and technical roles and the teams I've worked with have built systems that have processed $5b+ across compliant Web2 and Web3 rails. I've gotten to work with Chance over the years as he's enabled nearly a million gas optimized transactions and developed delightful software experiences.\n\nWe're excited to begin releasing product that we fully own and direct.\n\n## Meet Plug\n\nWe're not here to tell you to touch grass. We're here to build the command center for the terminally online. Your protocols, your strategies, your automations - all running exactly how you want them, when you want them. No more tab juggling required.\n\nFor the first time ever, you can:\n\n- Schedule constraints-driven transactions (because sometimes you need to sleep)\n- Access all major protocol functionality without bouncing between apps\n- Create and share strategies that actually make you money\n- Set up \"if this, then that\" executions that squeeze maximum value from every move\n\nOne interface to access Etherem, one interface to combine protocols, one interface to schedule transactions, and one interface to manage your onchain experience.\n\n## What's Coming\n\nWe're starting with Season 0 - our founding users phase. This initial release will be referral-gated, with priority access for everyone on the Plug waitlist. Being a founding user means more than just early access; it comes with built-in advantages that persist:\n\nYour transactions will be evaluated more frequently, leading to better execution. We'll subsidize your gas costs on L2s, and you'll get early access to new features as we roll them out.\n\nWe're taking a seasonal approach to give us flexibility - each season lasting between one to several months. This structure lets us integrate new protocols, test incentive alignment mechanisms, and evolve the product ecosystem based on what we learn together.\n\nWhen we transition to Season 1, our founding users keep their enhanced capabilities. We're building Plug for the long term, and we want our earliest users to remain at the forefront of its evolution.\n\n## The Future\n\nThis isn't just another DeFi tool. We're building the infrastructure for the next generation of onchain activity. A platform where users can compose, automate, and execute complex strategies without compromising on control or capability. This is about unlocking the true potential of being terminally online.\n\nReady to plug in? Join the waitlist at [onplug.io](https://www.onplug.io).\n",attributes:{created:"2025-01-28T06:00:00.000Z",className:"",tags:["perspective"],author:"danner"}}}; // * Get all the Posts for a given page. export const getPosts = ( diff --git a/packages/app/lib/tools.ts b/packages/app/lib/tools.ts index d004c0196..9bc3c954c 100644 --- a/packages/app/lib/tools.ts +++ b/packages/app/lib/tools.ts @@ -1,131 +1,49 @@ -import { getPrices } from "@/lib/functions/llama" import { getPositions } from "@/lib/functions/zerion/positions" import { schemas } from "./functions/plug" -import { intent } from "./functions/plug/solver" - -// Define the Intent type directly in this file -interface Intent { - chainId: number // e.g., 8453 for Base chain - from: string // User's address - inputs: Array<{ - protocol: string // e.g., "aave" - action: string // e.g., "supply" - token: string // Token address - amount: string // Amount to deposit - }> -} export const TOOLS: Record< string, { name: string description: string - execute: (socketId: string, socketAddress: string) => Promise + execute: (socketId: string, socketAddress: string, protocol?: string, action?: string) => Promise } > = { - schemas: { - name: "schemas", - description: - "List all available schemas in the system for integrated protocols that allow users to create workflows and bundle transactions. In order to use a specific schema the user needs to access the 'schema' tool", + introspection: { + name: "introspection", + description: "See what tools Morgan currently supports and has the ability to utilize in her chats with users.", execute: async () => { - try { - return await schemas(undefined, undefined, 8453, "") - } catch (error) { - console.error("Schema tool failed:", error) - return { error: "Failed to fetch schemas" } - } + return ` + Available tools: ${Object.keys(TOOLS).map(toolKey => `${TOOLS[toolKey].name}: ${TOOLS[toolKey].description}`)}. + When appropriate, suggest relevant tools from this list. + ` } }, - aave_supply: { - name: "aave_supply", + schema: { + name: "schema", description: - "Get the Aave supply schema which allows users to supply assets as collateral to Aave. Shows available tokens and their current supply APY.", - execute: async (_, socketAddress) => { - const result = await schemas("aave", "supply", 8453, socketAddress) - console.log("Aave Supply Schema Response:", JSON.stringify(result, null, 2)) - return result - } - }, - aave_borrow: { - name: "aave_borrow", - description: - "Get the Aave borrow schema which allows users to borrow assets against their supplied collateral. Shows available tokens and their current borrow APY.", - execute: async (_, socketAddress) => await schemas("aave", "borrow", 8453, socketAddress) - }, - aave_repay: { - name: "aave_repay", - description: - "Get the Aave repay schema which allows users to repay borrowed assets. Shows current borrowed positions that can be repaid.", - execute: async (_, socketAddress) => await schemas("aave", "repay", 8453, socketAddress) - }, - aave_withdraw: { - name: "aave_withdraw", - description: - "Get the Aave withdraw schema which allows users to withdraw their supplied assets. Shows current supplied positions that can be withdrawn.", - execute: async (_, socketAddress) => await schemas("aave", "withdraw", 8453, socketAddress) - }, - aave_deposit: { - name: "aave_deposit", - description: "Deposit assets into Aave V3.", - execute: async (socketId, socketAddress) => { - // Implement the deposit logic here - const depositAmount = "0.005" // Example amount - const tokenAddress = "0x..." // Example token address - - const depositIntent: Intent = { - chainId: 8453, - from: socketAddress, - inputs: [ - { - protocol: "aave", - action: "supply", - token: tokenAddress, - amount: depositAmount - } - ] - } - + "Get detailed information about a specific protocol's actions. Use this when a user asks about a specific protocol (protocol: X) or action (action: Y). This tool helps users understand what they can do with a particular protocol.", + execute: async (socketId, socketAddress, protocol, action) => { try { - const response = await intent(depositIntent) - return response + console.log("Schema tool called with:", protocol, action) + return await schemas(protocol, action, 8453, "") } catch (error) { - console.error("Deposit tool failed:", error) - return { error: "Failed to execute deposit" } + console.error("Schema tool failed:", error) + return { error: "Failed to fetch schemas" } } } }, - aave_health_factor: { - name: "aave_health_factor", - description: "Check the health factor for Aave V3.", - execute: async (socketId, socketAddress) => { - // Implement the health factor check logic here - } - }, - aave_apy: { - name: "aave_apy", - description: "Check the APY for Aave V3.", + schemas: { + name: "schemas", + description: + "Get an overview of all available protocols and their actions. Use this when a user is new or unsure what they want to do. The response will help guide them to specific protocols they can then explore with the schema tool.", execute: async (socketId, socketAddress) => { try { - const response = await intent({ - chainId: 1, // or whatever chain you're targeting - from: socketAddress, - inputs: [ - { - protocol: "aave_v3", - action: "apy", - direction: 1, // 1 for deposit, -1 for borrow - token: "0x...", // token address - operator: 1, // 1 for greater than, -1 for less than - threshold: "0.05" // 5% - } - ] - }) - - return response + return await schemas(undefined, undefined, 8453, "") } catch (error) { - console.error("APY tool failed:", error) - return { error: "Failed to fetch APY data" } + console.error("Schema tool failed:", error) + return { error: "Failed to fetch schemas" } } } }, @@ -147,52 +65,6 @@ export const TOOLS: Record< description: "Get the current fungible holding data for a specific socket (the Plug account of a connected wallet).", execute: async (socketId, socketAddress) => await getPositions(socketId, socketAddress) - }, - aave_monitor_borrow_apy: { - name: "aave_monitor_borrow_apy", - description: "Monitor Aave V3 borrow APY for a specific asset", - execute: async (socketId: string, socketAddress: string) => { - try { - // 1. First get the schema for the APY constraint - const schemaResponse = await intent({ - chainId: 8453, - from: socketAddress, - inputs: [ - { - protocol: "aave_v3", - action: "schemas" - } - ] - }) - - // 2. Find the APY constraint schema - const apySchema = schemaResponse.schemas.find((s: any) => s.action === "apy" && s.type === "constraint") - - if (!apySchema) { - return { error: "APY monitoring schema not found" } - } - - // 3. Return the schema info so the UI can collect inputs - return { - schema: apySchema, - requiredInputs: [ - { - name: "token", - type: "address", - description: "Asset to monitor (e.g. ETH, USDC)" - }, - { - name: "threshold", - type: "float", - description: "APY threshold percentage" - } - ] - } - } catch (error) { - console.error("Failed to get APY schema:", error) - return { error: "Failed to setup APY monitoring" } - } - } } // price: { // name: "price", @@ -201,5 +73,4 @@ export const TOOLS: Record< // return await getPrices(tokens) // } // }, - // TODO: Add specific tools for schema } diff --git a/packages/app/server/api/routers/biblo/chat.ts b/packages/app/server/api/routers/biblo/chat.ts index 0f0f768b5..8896a9ffd 100644 --- a/packages/app/server/api/routers/biblo/chat.ts +++ b/packages/app/server/api/routers/biblo/chat.ts @@ -3,11 +3,8 @@ import { TRPCError } from "@trpc/server" import { z } from "zod" import { Anthropic } from "@anthropic-ai/sdk" -import { Message } from "@prisma/client" import { env } from "@/env" -import { schemas } from "@/lib" -import { getPositions } from "@/lib/functions/zerion" import { TOOLS } from "@/lib/tools" import { createTRPCRouter, protectedProcedure } from "../../trpc" @@ -18,7 +15,6 @@ const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_KEY }) -// Add this before the parseDepositIntent function interface Intent { chainId: number from: string @@ -57,29 +53,29 @@ export const chat = createTRPCRouter({ } }) - // Parse deposit intent from user message - const depositIntent = parseDepositIntent(input.message) - if (depositIntent) { - // Create a Workflow based on the deposit intent - const workflow = await ctx.db.workflow.create({ - data: { - socketId: socket.id, - intentData: JSON.stringify(depositIntent), - createdAt: new Date() - } - }) - console.log("Workflow Created:", workflow) - - // Now create the Execution based on the Workflow - await ctx.db.execution.create({ - data: { - workflowId: workflow.id, - intentData: JSON.stringify(depositIntent), - createdAt: new Date() - } - }) - console.log("Execution Created for Workflow:", workflow.id) - } + // // Parse deposit intent from user message + // const depositIntent = parseDepositIntent(input.message) + // if (depositIntent) { + // // Create a Workflow based on the deposit intent + // const workflow = await ctx.db.workflow.create({ + // data: { + // socketId: socket.id, + // intentData: JSON.stringify(depositIntent), + // createdAt: new Date() + // } + // }) + // console.log("Workflow Created:", workflow) + // + // // Now create the Execution based on the Workflow + // await ctx.db.execution.create({ + // data: { + // workflowId: workflow.id, + // intentData: JSON.stringify(depositIntent), + // createdAt: new Date() + // } + // }) + // console.log("Execution Created for Workflow:", workflow.id) + // } const messages = input.history.map(msg => ({ role: msg.role, @@ -92,7 +88,7 @@ export const chat = createTRPCRouter({ }) const personality = ` - You are Piggy, a founder of Plug that is the user-facing member helping them be as successful as possible. + You are Morgan, a founder of Plug that is the user-facing member helping them be as successful as possible. - When making recommendations of what can be done, you only recommend data you have confirmed in your direct context. - When you are referencing a protocol retrieve the schema for that protocol and confirm the support we have for it. @@ -104,7 +100,13 @@ export const chat = createTRPCRouter({ This is very important, users do not use different apps and we do not send them anywhere else. We recommend they build a Plug when we have that protocol schema supported. - You do not use emojis in your responses. + When discussing protocols and actions: + 1. Always use the 'schema' tool when mentioning specific protocols or actions + 2. Use keywords 'protocol:' and 'action:' in your responses when referring to specific combinations + 3. Start new users with the 'schemas' tool to show available options + 4. When suggesting actions, format them as 'For protocol: X, you can use action: Y' + + You do not use emojis in your responses You stick to raw plain text styling. You understand that the user has a wallet with holdings that is seperate from their Plug account (Socket). You encourage the user to deposit their holdings into their Socket to use in Plugs. ` @@ -196,11 +198,44 @@ export const chat = createTRPCRouter({ async function executeTools(socketId: string, socketAddress: string, tools: string[], message: string) { const results: Record = {} + + // Enhanced protocol and action detection + const protocolMatch = message.match(/(?:protocol:|using|with|for|in|on)\s+(\w+)/i) + const actionMatch = message.match(/(?:action:|to|want to|would like to|can i)\s+(\w+)/i) + + // Also check assistant's last response for protocol: and action: keywords + const protocolKeywordMatch = message.match(/protocol:\s*(\w+)/i) + const actionKeywordMatch = message.match(/action:\s*(\w+)/i) + + const protocol = protocolKeywordMatch?.length + ? protocolKeywordMatch[1].toLowerCase() + : protocolMatch?.length + ? protocolMatch[1].toLowerCase() + : undefined + + const action = actionKeywordMatch?.length + ? actionKeywordMatch[1].toLowerCase() + : actionMatch?.length + ? actionMatch[1].toLowerCase() + : undefined + + console.log("Extracted protocol:", protocol, "action:", action) + for (const tool of tools) { if (tool in TOOLS) { try { - results[tool] = await TOOLS[tool].execute(socketId, socketAddress) - // If tool returned an error, log it but continue + if (tool === "schema") { + // If either protocol or action is specified, use schema tool + if (protocol || action) { + console.log("Using schema tool with:", protocol, action) + results[tool] = await TOOLS[tool].execute(socketId, socketAddress, protocol, action) + } else { + console.log("No protocol/action specified, using schemas tool") + results[tool] = await TOOLS["schemas"].execute(socketId, socketAddress) + } + } else { + results[tool] = await TOOLS[tool].execute(socketId, socketAddress) + } if (results[tool]?.error) { console.error(`Tool ${tool} failed:`, results[tool].error) } @@ -217,38 +252,3 @@ function parseToolSuggestions(text: string | undefined): string[] { if (!text) return [] return Object.keys(TOOLS).filter(tool => text.toLowerCase().includes(tool.toLowerCase())) } - -async function handleError(ctx, errorMessage, socketId) { - await ctx.db.message.create({ - data: { - socketId: socketId, - content: errorMessage, - isUser: false, - timeSent: new Date() - } - }) -} - -// Function to parse deposit intents from user messages -function parseDepositIntent(message: string): Intent | null { - const regex = /deposit (\d+(\.\d+)?) (\w+) to Aave/i // Regex to match "deposit 0.005 ETH to Aave" - const match = message.match(regex) - - if (match) { - const amount = match[1] - const token = match[3] - return { - chainId: 8453, - from: "", // Placeholder for user's address - inputs: [ - { - protocol: "aave", - action: "supply", - token, - amount - } - ] - } - } - return null // Return null if no match found -} From 1d6536206dc18fc53628c153ab62f22068b7d5e0 Mon Sep 17 00:00:00 2001 From: nftchance Date: Thu, 13 Feb 2025 08:16:53 -0600 Subject: [PATCH 5/6] feat: enter as submit --- .../app/columns/utils/column-chat.tsx | 77 ++++++++----------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/packages/app/components/app/columns/utils/column-chat.tsx b/packages/app/components/app/columns/utils/column-chat.tsx index 2e86e321c..b3caafa5a 100644 --- a/packages/app/components/app/columns/utils/column-chat.tsx +++ b/packages/app/components/app/columns/utils/column-chat.tsx @@ -11,7 +11,7 @@ import { api } from "@/server/client" import { Search } from "../../inputs/search" interface Message { - text: string + message: string isSent: boolean } @@ -27,37 +27,38 @@ const TypingIndicator = () => { ) } -const DEFAULT_MESSAGES = [ - "Help me open an Aave V3 position", - "What can I do with my tokens?", +const DEFAULT_TEXT = "👋 Hey, I'm Morgan. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited." +const DEFAULT_MESSAGE = { message: DEFAULT_TEXT, isSent: false } + +const QUICK_MESSAGES = [ + "What can I do with the tokens I hold?", + "What is the best place to earn yield?", "What can I do with Plug?" ] export const ColumnChat = ({ index }: { index: number }) => { + const [messages, setMessages] = useState([DEFAULT_MESSAGE]) const [message, setMessage] = useState("") - const [messages, setMessages] = useState(() => [ - { - text: "👋 Hey, I'm Morgan. I can answer nearly any question about anything you see here in Plug.\n\nMy knowledge may be limited.", - isSent: false - } - ]) + const [isTyping, setIsTyping] = useState(false) const [activeTools, setActiveTools] = useState([]) const chat = api.biblo.chat.message.useMutation() - const handleSubmit = async (sent: string) => { - if (!sent.trim()) return + const handleSubmit = async (e: React.FormEvent | React.MouseEvent) => { + e.preventDefault() - setMessages(prev => [...prev, { text: sent, isSent: true }]) + if (!message.trim()) return + + setMessages(prev => [...prev, { message: message, isSent: true }]) setIsTyping(true) setMessage("") try { const response = await chat.mutateAsync({ - message: sent, + message: message, history: messages.slice(-20).map(msg => ({ - content: msg.text, + content: msg.message, role: msg.isSent ? "user" : "assistant" })) }) @@ -68,22 +69,15 @@ export const ColumnChat = ({ index }: { index: number }) => { const fullResponse = [response.reply, ...response.additionalMessages].join("\n\n") - setMessages(prev => [...prev, { text: fullResponse, isSent: false }]) + setMessages(prev => [...prev, { message: fullResponse, isSent: false }]) } catch (error: any) { - console.error("Detailed Error:", { - error, - cause: error.cause, - data: error.data, - shape: error.shape - }) - const errorMessage = error.shape?.message || error.message || "Unknown error occurred" const errorCode = error.shape?.code || error.code setMessages(prev => [ ...prev, { - text: `Error Code ${errorCode}: ${errorMessage}`, + message: `Error Code ${errorCode}: ${errorMessage}`, isSent: false } ]) @@ -119,7 +113,7 @@ export const ColumnChat = ({ index }: { index: number }) => { p: ({ children }) =>

{children}

}} > - {msg.text} + {msg.message}
@@ -132,7 +126,7 @@ export const ColumnChat = ({ index }: { index: number }) => { @@ -154,12 +148,12 @@ export const ColumnChat = ({ index }: { index: number }) => { )}
- {DEFAULT_MESSAGES.map((msg, index) => ( + {QUICK_MESSAGES.map((msg, index) => (
- } - search={message} - handleSearch={message => setMessage(message)} - placeholder="Type a message..." - className="message-input" - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault() - handleSubmit(message) - } - }} - /> - +
+ } + search={message} + handleSearch={message => setMessage(message)} + placeholder="Type a message..." + /> + +
From 65a44bd52cafb1a246300a1da072e9f5a68f0a2c Mon Sep 17 00:00:00 2001 From: nftchance Date: Thu, 13 Feb 2025 08:34:34 -0600 Subject: [PATCH 6/6] chore: submission cleanup --- packages/app/components/app/columns/utils/column-chat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/components/app/columns/utils/column-chat.tsx b/packages/app/components/app/columns/utils/column-chat.tsx index b3caafa5a..51094159a 100644 --- a/packages/app/components/app/columns/utils/column-chat.tsx +++ b/packages/app/components/app/columns/utils/column-chat.tsx @@ -36,7 +36,7 @@ const QUICK_MESSAGES = [ "What can I do with Plug?" ] -export const ColumnChat = ({ index }: { index: number }) => { +export const ColumnChat = ({ }: { index: number }) => { const [messages, setMessages] = useState([DEFAULT_MESSAGE]) const [message, setMessage] = useState("") @@ -163,7 +163,7 @@ export const ColumnChat = ({ index }: { index: number }) => { ))}
-
+ } search={message}