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..51094159a
--- /dev/null
+++ b/packages/app/components/app/columns/utils/column-chat.tsx
@@ -0,0 +1,179 @@
+import { useEffect, useState } from "react"
+import Markdown from "react-markdown"
+
+import { SearchIcon } from "lucide-react"
+
+import { Button } from "@/components/shared/buttons/button"
+import { Counter } from "@/components/shared/utils/counter"
+import { cn, formatTitle } from "@/lib"
+import { api } from "@/server/client"
+
+import { Search } from "../../inputs/search"
+
+interface Message {
+ message: string
+ isSent: boolean
+}
+
+const TypingIndicator = () => {
+ return (
+
+ )
+}
+
+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: number }) => {
+ const [messages, setMessages] = useState
([DEFAULT_MESSAGE])
+ const [message, setMessage] = useState("")
+
+ const [isTyping, setIsTyping] = useState(false)
+ const [activeTools, setActiveTools] = useState([])
+
+ const chat = api.biblo.chat.message.useMutation()
+
+ const handleSubmit = async (e: React.FormEvent | React.MouseEvent) => {
+ e.preventDefault()
+
+ if (!message.trim()) return
+
+ setMessages(prev => [...prev, { message: message, isSent: true }])
+ setIsTyping(true)
+ setMessage("")
+
+ try {
+ const response = await chat.mutateAsync({
+ message: message,
+ history: messages.slice(-20).map(msg => ({
+ content: msg.message,
+ role: msg.isSent ? "user" : "assistant"
+ }))
+ })
+
+ if (response.tools?.length) {
+ setActiveTools(prev => [...new Set([...prev, ...response.tools])])
+ }
+
+ const fullResponse = [response.reply, ...response.additionalMessages].join("\n\n")
+
+ setMessages(prev => [...prev, { message: fullResponse, isSent: false }])
+ } catch (error: any) {
+ const errorMessage = error.shape?.message || error.message || "Unknown error occurred"
+ const errorCode = error.shape?.code || error.code
+
+ setMessages(prev => [
+ ...prev,
+ {
+ message: `Error Code ${errorCode}: ${errorMessage}`,
+ isSent: false
+ }
+ ])
+ } finally {
+ setIsTyping(false)
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+ {isTyping &&
}
+ {[...messages].reverse().map((msg, index) => (
+
+
+
(
+
+ ),
+ ol: ({ children }) => (
+ {children}
+ ),
+ li: ({ children }) => {children},
+ p: ({ children }) => {children}
+ }}
+ >
+ {msg.message}
+
+
+
+ ))}
+
+
+
+ {activeTools.length > 0 && (
+
+
+
+ {activeTools.map((tool, index) => (
+
+ ))}
+
+
+ )}
+
+
+ {QUICK_MESSAGES.map((msg, index) => (
+
+ ))}
+
+
+
+
+
+ >
+ )
+}
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/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
new file mode 100644
index 000000000..9bc3c954c
--- /dev/null
+++ b/packages/app/lib/tools.ts
@@ -0,0 +1,76 @@
+import { getPositions } from "@/lib/functions/zerion/positions"
+
+import { schemas } from "./functions/plug"
+
+export const TOOLS: Record<
+ string,
+ {
+ name: string
+ description: string
+ execute: (socketId: string, socketAddress: string, protocol?: string, action?: string) => Promise
+ }
+> = {
+ introspection: {
+ name: "introspection",
+ description: "See what tools Morgan currently supports and has the ability to utilize in her chats with users.",
+ execute: async () => {
+ return `
+ Available tools: ${Object.keys(TOOLS).map(toolKey => `${TOOLS[toolKey].name}: ${TOOLS[toolKey].description}`)}.
+ When appropriate, suggest relevant tools from this list.
+ `
+ }
+ },
+ schema: {
+ name: "schema",
+ description:
+ "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 {
+ console.log("Schema tool called with:", protocol, action)
+ return await schemas(protocol, action, 8453, "")
+ } catch (error) {
+ console.error("Schema tool failed:", error)
+ return { error: "Failed to fetch schemas" }
+ }
+ }
+ },
+ 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 {
+ return await schemas(undefined, undefined, 8453, "")
+ } catch (error) {
+ console.error("Schema tool failed:", error)
+ return { error: "Failed to fetch schemas" }
+ }
+ }
+ },
+ 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)
+ }
+ // price: {
+ // name: "price",
+ // description: "Get current price information for specific tokens",
+ // execute: async () => {
+ // return await getPrices(tokens)
+ // }
+ // },
+}
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/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..8896a9ffd
--- /dev/null
+++ b/packages/app/server/api/routers/biblo/chat.ts
@@ -0,0 +1,254 @@
+import { TRPCError } from "@trpc/server"
+
+import { z } from "zod"
+
+import { Anthropic } from "@anthropic-ai/sdk"
+
+import { env } from "@/env"
+import { TOOLS } from "@/lib/tools"
+
+import { createTRPCRouter, protectedProcedure } from "../../trpc"
+
+const CLAUDE_MODEL = "claude-3-5-haiku-20241022"
+
+const anthropic = new Anthropic({
+ apiKey: env.ANTHROPIC_KEY
+})
+
+interface Intent {
+ chainId: number
+ from: string
+ inputs: Array<{
+ protocol: string
+ action: string
+ token: string
+ amount: string
+ }>
+}
+
+export const chat = createTRPCRouter({
+ message: protectedProcedure
+ .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",
+ content: input.message
+ })
+
+ const personality = `
+ 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.
+ - 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.
+
+ 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.
+ `
+
+ 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: 512,
+ messages,
+ system: `
+ ${personality}
+ ${tools}
+ ${user}
+ `
+ })
+ 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(
+ 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",
+ 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: `
+ ${personality}
+ ${user}
+ `
+ })
+
+ const finalMessages = finalResponse.content
+ .filter(content => content.type === "text")
+ .map(content => content.text)
+
+ return {
+ reply: finalMessages[0],
+ additionalMessages: finalMessages.slice(1),
+ tools: neededTools
+ }
+ }
+
+ return {
+ 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(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 {
+ 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)
+ }
+ } catch (error) {
+ console.error(`Tool ${tool} execution failed:`, error)
+ results[tool] = { error: `Failed to execute ${tool}` }
+ }
+ }
+ }
+ return results
+}
+
+function parseToolSuggestions(text: string | undefined): string[] {
+ if (!text) return []
+ return Object.keys(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,
+})
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",
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))