87 | >(({ className, ...props }, ref) => (
88 | | [role=checkbox]]:translate-y-[2px]',
92 | className
93 | )}
94 | {...props}
95 | />
96 | ));
97 | TableCell.displayName = 'TableCell';
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | TableCaption.displayName = 'TableCaption';
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | };
121 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
7 | line-height: 1.5;
8 | font-weight: 400;
9 |
10 | color-scheme: light dark;
11 | color: rgba(255, 255, 255, 0.87);
12 | background-color: #242424;
13 |
14 | font-synthesis: none;
15 | text-rendering: optimizeLegibility;
16 | -webkit-font-smoothing: antialiased;
17 | -moz-osx-font-smoothing: grayscale;
18 | }
19 |
20 | a {
21 | font-weight: 500;
22 | color: #646cff;
23 | text-decoration: inherit;
24 | }
25 |
26 | a:hover {
27 | color: #535bf2;
28 | }
29 |
30 | body {
31 | margin: 0;
32 | padding: 0;
33 | min-height: 100vh;
34 | min-width: 100vw;
35 | overflow: hidden;
36 | }
37 |
38 | h1 {
39 | font-size: 3.2em;
40 | line-height: 1.1;
41 | }
42 |
43 | button {
44 | border-radius: 8px;
45 | border: 1px solid transparent;
46 | padding: 0.6em 1.2em;
47 | font-size: 1em;
48 | font-weight: 500;
49 | font-family: inherit;
50 | background-color: #1a1a1a;
51 | cursor: pointer;
52 | transition: border-color 0.25s;
53 | }
54 |
55 | button:hover {
56 | border-color: #646cff;
57 | }
58 |
59 | button:focus,
60 | button:focus-visible {
61 | outline: 4px auto -webkit-focus-ring-color;
62 | }
63 |
64 | @media (prefers-color-scheme: light) {
65 | :root {
66 | color: #213547;
67 | background-color: #ffffff;
68 | }
69 |
70 | a:hover {
71 | color: #747bff;
72 | }
73 |
74 | button {
75 | background-color: #f9f9f9;
76 | }
77 | }
78 |
79 | @layer base {
80 | :root {
81 | --background: 0 0% 100%;
82 | --foreground: 0 0% 3.9%;
83 | --card: 0 0% 100%;
84 | --card-foreground: 0 0% 3.9%;
85 | --popover: 0 0% 100%;
86 | --popover-foreground: 0 0% 3.9%;
87 | --primary: 0 0% 9%;
88 | --primary-foreground: 0 0% 98%;
89 | --secondary: 0 0% 96.1%;
90 | --secondary-foreground: 0 0% 9%;
91 | --muted: 0 0% 96.1%;
92 | --muted-foreground: 0 0% 45.1%;
93 | --accent: 0 0% 96.1%;
94 | --accent-foreground: 0 0% 9%;
95 | --destructive: 0 84.2% 60.2%;
96 | --destructive-foreground: 0 0% 98%;
97 | --border: 0 0% 89.8%;
98 | --input: 0 0% 89.8%;
99 | --ring: 0 0% 3.9%;
100 | --chart-1: 12 76% 61%;
101 | --chart-2: 173 58% 39%;
102 | --chart-3: 197 37% 24%;
103 | --chart-4: 43 74% 66%;
104 | --chart-5: 27 87% 67%;
105 | --radius: 0.5rem;
106 | }
107 |
108 | .dark {
109 | --background: 0 0% 3.9%;
110 | --foreground: 0 0% 98%;
111 | --card: 0 0% 3.9%;
112 | --card-foreground: 0 0% 98%;
113 | --popover: 0 0% 3.9%;
114 | --popover-foreground: 0 0% 98%;
115 | --primary: 0 0% 98%;
116 | --primary-foreground: 0 0% 9%;
117 | --secondary: 0 0% 14.9%;
118 | --secondary-foreground: 0 0% 98%;
119 | --muted: 0 0% 14.9%;
120 | --muted-foreground: 0 0% 63.9%;
121 | --accent: 0 0% 14.9%;
122 | --accent-foreground: 0 0% 98%;
123 | --destructive: 0 62.8% 30.6%;
124 | --destructive-foreground: 0 0% 98%;
125 | --border: 0 0% 14.9%;
126 | --input: 0 0% 14.9%;
127 | --ring: 0 0% 83.1%;
128 | --chart-1: 220 70% 50%;
129 | --chart-2: 160 60% 45%;
130 | --chart-3: 30 80% 55%;
131 | --chart-4: 280 65% 60%;
132 | --chart-5: 340 75% 55%;
133 | }
134 | }
135 |
136 | @layer base {
137 | * {
138 | @apply border-border;
139 | }
140 |
141 | body {
142 | @apply bg-background text-foreground;
143 | }
144 | }
--------------------------------------------------------------------------------
/client/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Drawer as DrawerPrimitive } from 'vaul';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const Drawer = ({
7 | shouldScaleBackground = true,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
14 | );
15 | Drawer.displayName = 'Drawer';
16 |
17 | const DrawerTrigger = DrawerPrimitive.Trigger;
18 |
19 | const DrawerPortal = DrawerPrimitive.Portal;
20 |
21 | const DrawerClose = DrawerPrimitive.Close;
22 |
23 | const DrawerOverlay = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
34 |
35 | const DrawerContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, ...props }, ref) => (
39 |
40 |
41 |
49 |
50 | {children}
51 |
52 |
53 | ));
54 | DrawerContent.displayName = 'DrawerContent';
55 |
56 | const DrawerHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
64 | );
65 | DrawerHeader.displayName = 'DrawerHeader';
66 |
67 | const DrawerFooter = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
75 | );
76 | DrawerFooter.displayName = 'DrawerFooter';
77 |
78 | const DrawerTitle = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef
81 | >(({ className, ...props }, ref) => (
82 |
90 | ));
91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
92 |
93 | const DrawerDescription = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
102 | ));
103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
104 |
105 | export {
106 | Drawer,
107 | DrawerPortal,
108 | DrawerOverlay,
109 | DrawerTrigger,
110 | DrawerClose,
111 | DrawerContent,
112 | DrawerHeader,
113 | DrawerFooter,
114 | DrawerTitle,
115 | DrawerDescription,
116 | };
117 |
--------------------------------------------------------------------------------
/app/styles/css.py:
--------------------------------------------------------------------------------
1 | """CSS styles definition module"""
2 |
3 | def get_css_styles():
4 | """Return application CSS styles"""
5 | return """
6 |
127 | """
--------------------------------------------------------------------------------
/ai/llm/openai_chat.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Dict, Any, Iterator, AsyncIterator, Union
2 | from openai import OpenAI, AsyncOpenAI
3 | from openai.types.chat import ChatCompletion
4 | from phi.llm.openai import OpenAIChat as PhiOpenAIChat
5 | from openai import OpenAI
6 | import logging
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | class CustomOpenAIChat(PhiOpenAIChat):
11 | def __init__(self, api_key: str, base_url: Optional[str] = None, **kwargs):
12 | super().__init__(**kwargs)
13 | # Initialize OpenAI client with custom base URL and API key
14 | client_params = {
15 | "api_key": api_key,
16 | }
17 |
18 | if base_url:
19 | client_params["base_url"] = base_url
20 |
21 | self.client = OpenAI(**client_params)
22 |
23 | def get_client(self) -> OpenAI:
24 | """Override to return our custom client"""
25 | return self.client
26 |
27 | async def get_async_client(self) -> AsyncOpenAI:
28 | """Returns a custom async OpenAI client"""
29 |
30 | # Get settings from environment variables or configuration
31 | api_key = self.api_key
32 | base_url = self.base_url
33 | organization = self.organization
34 |
35 | # Create client configuration
36 | client_kwargs: Dict[str, Any] = {
37 | "api_key": api_key,
38 | "timeout": self.timeout,
39 | # Add custom headers directly in the client configuration
40 | "default_headers": {
41 | "User-Agent": "CustomOpenAIChat/1.0"
42 | }
43 | }
44 |
45 | # Add optional configuration
46 | if base_url:
47 | client_kwargs["base_url"] = base_url
48 | if organization:
49 | client_kwargs["organization"] = organization
50 |
51 | # Create custom async client
52 | return AsyncOpenAI(**client_kwargs)
53 |
54 | def create_completion(self, stream: bool = False) -> Union[ChatCompletion, Iterator[str]]:
55 | """Create chat completion"""
56 | try:
57 | client = self.get_client()
58 | response = client.chat.completions.create(
59 | model=self.model,
60 | messages=self.messages,
61 | stream=stream,
62 | **self.completion_kwargs
63 | )
64 |
65 | if stream:
66 | return self._process_stream_response(response)
67 | return response
68 |
69 | except Exception as e:
70 | logger.error(f"Failed to create chat completion: {e}")
71 | raise
72 |
73 | async def acreate_completion(self, stream: bool = False) -> Union[ChatCompletion, AsyncIterator[str]]:
74 | """Create async chat completion"""
75 | try:
76 | client = await self.get_async_client()
77 | response = await client.chat.completions.create(
78 | model=self.model,
79 | messages=self.messages,
80 | stream=stream,
81 | **self.completion_kwargs
82 | )
83 |
84 | if stream:
85 | return self._process_async_stream_response(response)
86 | return response
87 |
88 | except Exception as e:
89 | logger.error(f"Failed to create async chat completion: {e}")
90 | raise
91 |
92 | def _process_stream_response(self, response: Iterator[ChatCompletion]) -> Iterator[str]:
93 | """Process stream response"""
94 | for chunk in response:
95 | if chunk.choices[0].delta.content is not None:
96 | yield chunk.choices[0].delta.content
97 |
98 | async def _process_async_stream_response(self, response: AsyncIterator[ChatCompletion]) -> AsyncIterator[str]:
99 | """Process async stream response"""
100 | async for chunk in response:
101 | if chunk.choices[0].delta.content is not None:
102 | yield chunk.choices[0].delta.content
--------------------------------------------------------------------------------
/client/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as DialogPrimitive from '@radix-ui/react-dialog';
3 | import { Cross2Icon } from '@radix-ui/react-icons';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const Dialog = DialogPrimitive.Root;
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger;
10 |
11 | const DialogPortal = DialogPrimitive.Portal;
12 |
13 | const DialogClose = DialogPrimitive.Close;
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ));
52 | DialogContent.displayName = DialogPrimitive.Content.displayName;
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | );
66 | DialogHeader.displayName = 'DialogHeader';
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | );
80 | DialogFooter.displayName = 'DialogFooter';
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ));
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ));
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogTrigger,
114 | DialogClose,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | };
121 |
--------------------------------------------------------------------------------
/app/db/sqlite.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 | import json
4 | from datetime import datetime
5 | from typing import Optional, List, Dict, Any
6 | from .base import BaseStorage
7 | from .config import db_settings
8 |
9 | class SQLiteStorage(BaseStorage):
10 | def __init__(self, db_path: Optional[str] = None):
11 | """Initialize SQLite storage"""
12 | if db_path is None:
13 | db_path = db_settings.absolute_db_path
14 |
15 | self.db_path = db_path
16 | self._init_db()
17 |
18 | def _init_db(self):
19 | """Initialize database tables"""
20 | # Ensure directory exists
21 | os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
22 |
23 | with sqlite3.connect(self.db_path) as conn:
24 | conn.execute("""
25 | CREATE TABLE IF NOT EXISTS assistant_runs (
26 | run_id TEXT PRIMARY KEY,
27 | user_id TEXT,
28 | assistant_name TEXT,
29 | created_at TIMESTAMP,
30 | messages TEXT,
31 | metadata TEXT
32 | )
33 | """)
34 | conn.commit()
35 |
36 | def save_run(
37 | self,
38 | run_id: str,
39 | messages: List[Dict[str, Any]],
40 | metadata: Optional[Dict[str, Any]] = None,
41 | user_id: Optional[str] = None,
42 | assistant_name: Optional[str] = None,
43 | ) -> None:
44 | """Save a conversation run"""
45 | with sqlite3.connect(self.db_path) as conn:
46 | conn.execute(
47 | """
48 | INSERT OR REPLACE INTO assistant_runs
49 | (run_id, user_id, assistant_name, created_at, messages, metadata)
50 | VALUES (?, ?, ?, ?, ?, ?)
51 | """,
52 | (
53 | run_id,
54 | user_id,
55 | assistant_name,
56 | datetime.utcnow().isoformat(),
57 | json.dumps(messages),
58 | json.dumps(metadata or {})
59 | )
60 | )
61 | conn.commit()
62 |
63 | def get_run(self, run_id: str) -> Optional[Dict[str, Any]]:
64 | """Get a conversation run by ID"""
65 | with sqlite3.connect(self.db_path) as conn:
66 | conn.row_factory = sqlite3.Row
67 | cursor = conn.execute(
68 | "SELECT * FROM assistant_runs WHERE run_id = ?",
69 | (run_id,)
70 | )
71 | row = cursor.fetchone()
72 |
73 | if row:
74 | return {
75 | "run_id": row["run_id"],
76 | "user_id": row["user_id"],
77 | "assistant_name": row["assistant_name"],
78 | "created_at": row["created_at"],
79 | "messages": json.loads(row["messages"]),
80 | "metadata": json.loads(row["metadata"])
81 | }
82 | return None
83 |
84 | def delete_run(self, run_id: str) -> None:
85 | """Delete a conversation run"""
86 | with sqlite3.connect(self.db_path) as conn:
87 | conn.execute("DELETE FROM assistant_runs WHERE run_id = ?", (run_id,))
88 | conn.commit()
89 |
90 | def get_all_runs(self) -> List[Dict[str, Any]]:
91 | """Get all conversation runs"""
92 | with sqlite3.connect(self.db_path) as conn:
93 | conn.row_factory = sqlite3.Row
94 | cursor = conn.execute("SELECT * FROM assistant_runs ORDER BY created_at DESC")
95 | rows = cursor.fetchall()
96 |
97 | return [
98 | {
99 | "run_id": row["run_id"],
100 | "user_id": row["user_id"],
101 | "assistant_name": row["assistant_name"],
102 | "created_at": row["created_at"],
103 | "messages": json.loads(row["messages"]),
104 | "metadata": json.loads(row["metadata"])
105 | }
106 | for row in rows
107 | ]
108 |
109 | def get_all_run_ids(self) -> List[str]:
110 | """Get all run IDs"""
111 | with sqlite3.connect(self.db_path) as conn:
112 | cursor = conn.execute("SELECT run_id FROM assistant_runs ORDER BY created_at DESC")
113 | return [row[0] for row in cursor.fetchall()]
--------------------------------------------------------------------------------
/mcp_solana/README.md:
--------------------------------------------------------------------------------
1 | # MCP Solana Integration
2 |
3 | This package provides Model Context Protocol (MCP) integration for Solana blockchain operations. It allows AI applications like Claude Desktop and Cursor to interact with the Solana blockchain through the MCP protocol.
4 |
5 | ## Features
6 |
7 | - **Wallet Management**: Retrieve wallet addresses and check balances
8 | - **Transaction Operations**: Transfer SOL and tokens between wallets
9 | - **Smart Contract Interactions**: Deploy and call smart contracts
10 | - **Token Management**: List token accounts and manage tokens
11 |
12 | ## Architecture
13 |
14 | The MCP Solana integration follows the client-server architecture of the Model Context Protocol:
15 |
16 | - **MCP Client**: Connects to an MCP server to access Solana capabilities
17 | - **MCP Server**: Provides Solana blockchain operations to MCP clients
18 | - **Transport Layer**: Supports both local (stdio) and remote (HTTP/SSE) communication
19 |
20 | ## Installation
21 |
22 | ```bash
23 | # Install from the repository
24 | pip install -e .
25 |
26 | # Or install with dependencies
27 | pip install -e ".[dev]"
28 | ```
29 |
30 | ## Usage
31 |
32 | ### Client Usage
33 |
34 | ```python
35 | import asyncio
36 | from mcp_solana.client.client import SolanaMCPClient
37 |
38 | async def main():
39 | # Create a client with HTTP transport
40 | client = SolanaMCPClient(server_url="http://localhost:8080")
41 |
42 | try:
43 | # Initialize the client
44 | await client.initialize()
45 |
46 | # Get wallet address
47 | address = await client.get_wallet_address()
48 | print(f"Wallet address: {address}")
49 |
50 | # Get wallet balance
51 | balance = await client.get_wallet_balance(address)
52 | print(f"Wallet balance: {balance}")
53 |
54 | # Transfer SOL
55 | recipient = "RECIPIENT_ADDRESS_HERE"
56 | amount = 0.01 # SOL
57 | tx = await client.transfer_sol(recipient, amount)
58 | print(f"Transfer transaction: {tx}")
59 |
60 | finally:
61 | # Close the client
62 | await client.close()
63 |
64 | if __name__ == "__main__":
65 | asyncio.run(main())
66 | ```
67 |
68 | ### Server Configuration
69 |
70 | To run the MCP Solana server:
71 |
72 | ```bash
73 | # Start the server with default configuration
74 | python -m mcp_solana.server.server
75 |
76 | # Start the server with custom configuration
77 | python -m mcp_solana.server.server --port 8080 --rpc-url https://api.mainnet-beta.solana.com
78 | ```
79 |
80 | ## Integration with AI Applications
81 |
82 | ### Claude Desktop Integration
83 |
84 | 1. Install the MCP Solana server:
85 | ```bash
86 | pip install mcp-solana
87 | ```
88 |
89 | 2. Add the following to your MCP configuration file:
90 | ```json
91 | {
92 | "solana": {
93 | "command": "python",
94 | "args": ["-m", "mcp_solana.server.server"]
95 | }
96 | }
97 | ```
98 |
99 | 3. Start Claude Desktop and use the Solana capabilities:
100 | ```
101 | You: Can you check my Solana wallet balance?
102 | Claude: I'll check your Solana wallet balance for you.
103 | [Claude uses MCP to connect to the Solana blockchain]
104 | Your wallet address is 7Xnw...3jUP and has a balance of 1.45 SOL.
105 | ```
106 |
107 | ### Cursor Integration
108 |
109 | 1. Install the MCP Solana server:
110 | ```bash
111 | pip install mcp-solana
112 | ```
113 |
114 | 2. Add the following to your MCP configuration file:
115 | ```json
116 | {
117 | "solana": {
118 | "command": "python",
119 | "args": ["-m", "mcp_solana.server.server"]
120 | }
121 | }
122 | ```
123 |
124 | 3. Start Cursor and use the Solana capabilities in your development workflow.
125 |
126 | ## Development
127 |
128 | ### Project Structure
129 |
130 | ```
131 | mcp_solana/
132 | ├── client/ # MCP client implementation
133 | ├── server/ # MCP server implementation
134 | ├── models/ # Data models
135 | ├── utils/ # Utility functions
136 | └── examples/ # Example scripts
137 | ```
138 |
139 | ### Running Tests
140 |
141 | ```bash
142 | # Run all tests
143 | pytest
144 |
145 | # Run specific tests
146 | pytest tests/test_client.py
147 | ```
148 |
149 | ### Building Documentation
150 |
151 | ```bash
152 | # Build documentation
153 | cd docs
154 | make html
155 | ```
156 |
157 | ## License
158 |
159 | This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
--------------------------------------------------------------------------------
/app/api/routes/tools.py:
--------------------------------------------------------------------------------
1 | """
2 | API routes for the Tool Integration Protocol.
3 | """
4 |
5 | from typing import Any, Dict, List, Optional
6 |
7 | from fastapi import APIRouter, Depends, HTTPException, Query
8 |
9 | from app.api.auth import get_current_user
10 | from app.db.memory import MemoryDatabase
11 | from app.tools.protocol.executor import ToolExecutionError, ToolExecutor
12 | from app.tools.protocol.models import ToolImplementation, ToolManifest, ValidationResult
13 | from app.tools.protocol.registry import ToolRegistry
14 |
15 | router = APIRouter()
16 |
17 | # Initialize database and registry
18 | db = MemoryDatabase()
19 | registry = ToolRegistry(db)
20 | executor = ToolExecutor(registry)
21 |
22 |
23 | @router.post("/register", response_model=Dict[str, Any])
24 | async def register_tool(
25 | manifest: ToolManifest,
26 | implementation: ToolImplementation,
27 | user = Depends(get_current_user)
28 | ):
29 | """
30 | Register a new tool or update an existing tool.
31 | """
32 | try:
33 | # Validate manifest
34 | validation_result = registry.validate_manifest(manifest)
35 | if not validation_result.is_valid:
36 | raise HTTPException(status_code=400, detail=validation_result.errors)
37 |
38 | # Register tool
39 | tool_id = await registry.register(manifest, implementation)
40 |
41 | return {"tool_id": tool_id, "status": "registered"}
42 | except Exception as e:
43 | raise HTTPException(status_code=500, detail=str(e))
44 |
45 |
46 | @router.get("/list", response_model=List[Dict[str, Any]])
47 | async def list_tools(
48 | category: Optional[str] = Query(None, description="Filter by category"),
49 | tag: Optional[str] = Query(None, description="Filter by tag"),
50 | user = Depends(get_current_user)
51 | ):
52 | """
53 | List registered tools with optional filtering.
54 | """
55 | try:
56 | filters = {}
57 | if category:
58 | filters["manifest.categories"] = category
59 | if tag:
60 | filters["manifest.tags"] = tag
61 |
62 | tools = await registry.list_tools(filters)
63 |
64 | return tools
65 | except Exception as e:
66 | raise HTTPException(status_code=500, detail=str(e))
67 |
68 |
69 | @router.get("/{tool_id}", response_model=Dict[str, Any])
70 | async def get_tool(
71 | tool_id: str,
72 | user = Depends(get_current_user)
73 | ):
74 | """
75 | Get a tool by ID.
76 | """
77 | try:
78 | tool = await registry.get_tool(tool_id)
79 | if not tool:
80 | raise HTTPException(status_code=404, detail=f"Tool not found: {tool_id}")
81 |
82 | return tool
83 | except Exception as e:
84 | raise HTTPException(status_code=500, detail=str(e))
85 |
86 |
87 | @router.delete("/{tool_id}", response_model=Dict[str, Any])
88 | async def delete_tool(
89 | tool_id: str,
90 | user = Depends(get_current_user)
91 | ):
92 | """
93 | Delete a tool.
94 | """
95 | try:
96 | deleted = await registry.delete_tool(tool_id)
97 | if not deleted:
98 | raise HTTPException(status_code=404, detail=f"Tool not found: {tool_id}")
99 |
100 | return {"tool_id": tool_id, "status": "deleted"}
101 | except Exception as e:
102 | raise HTTPException(status_code=500, detail=str(e))
103 |
104 |
105 | @router.post("/{tool_id}/capabilities/{capability_id}/execute", response_model=Dict[str, Any])
106 | async def execute_capability(
107 | tool_id: str,
108 | capability_id: str,
109 | parameters: Dict[str, Any],
110 | context: Optional[Dict[str, Any]] = None,
111 | user = Depends(get_current_user)
112 | ):
113 | """
114 | Execute a tool capability.
115 | """
116 | try:
117 | # Get tool
118 | tool = await registry.get_tool(tool_id)
119 | if not tool:
120 | raise HTTPException(status_code=404, detail=f"Tool not found: {tool_id}")
121 |
122 | # Execute capability
123 | result = await executor.execute(
124 | tool_id=tool_id,
125 | capability_id=capability_id,
126 | parameters=parameters,
127 | context=context or {},
128 | user_id=user.id if user else None
129 | )
130 |
131 | return result
132 | except ToolExecutionError as e:
133 | raise HTTPException(status_code=500, detail=str(e))
134 | except Exception as e:
135 | raise HTTPException(status_code=500, detail=str(e))
--------------------------------------------------------------------------------
/app/db/init.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from typing import Optional
4 | import sqlite3
5 | import psycopg
6 | from pathlib import Path
7 | from .config import db_settings
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | def init_sqlite_db(db_path: str) -> bool:
12 | """Initialize SQLite database and tables"""
13 | try:
14 | # Ensure data directory exists and has correct permissions
15 | db_dir = os.path.dirname(db_path)
16 | os.makedirs(db_dir, exist_ok=True)
17 |
18 | logger.info(f"Initializing SQLite database at {db_path}")
19 |
20 | # Test file write permission
21 | if os.path.exists(db_path):
22 | if not os.access(db_path, os.W_OK):
23 | logger.error(f"No write permission for database file: {db_path}")
24 | return False
25 | else:
26 | # Test directory write permission
27 | if not os.access(db_dir, os.W_OK):
28 | logger.error(f"No write permission for directory: {db_dir}")
29 | return False
30 |
31 | with sqlite3.connect(db_path) as conn:
32 | # Assistant runs table
33 | conn.execute("""
34 | CREATE TABLE IF NOT EXISTS assistant_runs (
35 | run_id TEXT PRIMARY KEY,
36 | user_id TEXT,
37 | assistant_name TEXT,
38 | created_at TIMESTAMP,
39 | messages TEXT,
40 | metadata TEXT
41 | )
42 | """)
43 | conn.commit()
44 |
45 | logger.info("SQLite database initialized successfully")
46 | return True
47 |
48 | except Exception as e:
49 | logger.error(f"Failed to initialize SQLite database: {str(e)}")
50 | return False
51 |
52 | def init_postgres_db(db_url: str):
53 | """Initialize PostgreSQL database and tables"""
54 | logger.info("Initializing PostgreSQL database")
55 |
56 | # First try to create database if it doesn't exist
57 | db_name = db_settings.POSTGRES_DB
58 | base_url = db_url.rsplit('/', 1)[0]
59 |
60 | try:
61 | with psycopg.connect(f"{base_url}/postgres") as conn:
62 | conn.autocommit = True
63 | with conn.cursor() as cur:
64 | # Check if database exists
65 | cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (db_name,))
66 | if not cur.fetchone():
67 | cur.execute(f"CREATE DATABASE {db_name}")
68 | logger.info(f"Created database {db_name}")
69 | except Exception as e:
70 | logger.warning(f"Could not create database: {e}")
71 |
72 | # Now create tables
73 | try:
74 | with psycopg.connect(db_url) as conn:
75 | conn.autocommit = True
76 | with conn.cursor() as cur:
77 | # Assistant runs table
78 | cur.execute("""
79 | CREATE TABLE IF NOT EXISTS assistant_runs (
80 | run_id TEXT PRIMARY KEY,
81 | user_id TEXT,
82 | assistant_name TEXT,
83 | created_at TIMESTAMP,
84 | messages JSONB,
85 | metadata JSONB
86 | )
87 | """)
88 |
89 | # Vector storage table
90 | cur.execute("""
91 | CREATE TABLE IF NOT EXISTS vectors (
92 | id TEXT PRIMARY KEY,
93 | collection TEXT,
94 | embedding vector(1536),
95 | metadata JSONB
96 | )
97 | """)
98 | except Exception as e:
99 | logger.error(f"Error creating tables: {e}")
100 | raise
101 |
102 | def init_database() -> bool:
103 | """Initialize database based on configuration"""
104 | if db_settings.AUTO_CREATE_DB:
105 | try:
106 | if db_settings.is_sqlite:
107 | return init_sqlite_db(db_settings.absolute_db_path)
108 | elif db_settings.is_postgres:
109 | return init_postgres_db(db_settings.db_url)
110 | else:
111 | logger.error(f"Unsupported database type: {db_settings.DATABASE_TYPE}")
112 | return False
113 | except Exception as e:
114 | logger.error(f"Database initialization failed: {e}")
115 | return False
116 | return True
--------------------------------------------------------------------------------
/app/components/chat.py:
--------------------------------------------------------------------------------
1 | """Chat interface component module"""
2 |
3 | import streamlit as st
4 | from phi.assistant import Assistant
5 | from phi.utils.log import logger
6 | from ai.assistants import get_lyraios
7 | from app.utils.session import get_tool_states
8 | from app.db.models import ChatMessage
9 | from app.config.settings import chat_settings
10 | from datetime import datetime
11 |
12 | def init_assistant(user_id: str) -> Assistant:
13 | """Initialize assistant"""
14 | if "lyraios" not in st.session_state or st.session_state["lyraios"] is None:
15 | with st.spinner("Initializing LYRAIOS..."):
16 | logger.info("---*--- Creating LYRAIOS ---*---")
17 | try:
18 | tool_states = get_tool_states()
19 | lyraios = get_lyraios(
20 | user_id=user_id,
21 | calculator=tool_states["calculator"],
22 | ddg_search=tool_states["ddg_search"],
23 | file_tools=tool_states["file_tools"],
24 | finance_tools=tool_states["finance_tools"],
25 | research_assistant=tool_states["research_assistant"],
26 | )
27 | st.session_state["lyraios"] = lyraios
28 | except Exception as e:
29 | st.error(f"Failed to create assistant: {str(e)}")
30 | logger.error(f"Failed to create assistant: {e}")
31 | return None
32 |
33 | return st.session_state["lyraios"]
34 |
35 | def init_chat_history(lyraios: Assistant):
36 | """Initialize chat history"""
37 | if "messages" not in st.session_state:
38 | assistant_chat_history = lyraios.memory.get_chat_history()
39 | if len(assistant_chat_history) > 0:
40 | logger.debug("Loading chat history")
41 | st.session_state["messages"] = assistant_chat_history
42 | else:
43 | logger.debug("No chat history found")
44 | st.session_state["messages"] = [{"role": "assistant", "content": "Hello! I'm LYRAIOS, an advanced AI assistant. I can help you find information, answer questions, perform calculations and more. Please let me know what you need help with?"}]
45 |
46 | def create_assistant_run(lyraios: Assistant):
47 | """Create assistant run"""
48 | try:
49 | if "lyraios_run_id" not in st.session_state or st.session_state["lyraios_run_id"] is None:
50 | run_id = lyraios.create_run()
51 | st.session_state["lyraios_run_id"] = run_id
52 | logger.info(f"Created new run: {run_id}")
53 | except Exception as e:
54 | st.error(f"Failed to create assistant run: {str(e)}")
55 | logger.error(f"Failed to create run: {e}")
56 | return False
57 |
58 | return True
59 |
60 | def render_chat_interface(user_id: str):
61 | """Render chat interface"""
62 | # Initialize assistant
63 | lyraios = init_assistant(user_id)
64 | if lyraios is None:
65 | return
66 |
67 | # Create assistant run
68 | if not create_assistant_run(lyraios):
69 | return
70 |
71 | # Initialize chat history
72 | init_chat_history(lyraios)
73 |
74 | # Create chat interface container
75 | chat_container = st.container()
76 |
77 | # Display existing chat messages
78 | with chat_container:
79 | for message in st.session_state["messages"]:
80 | if message["role"] == "system":
81 | continue
82 | with st.chat_message(message["role"]):
83 | st.markdown(message["content"])
84 |
85 | # User input
86 | if prompt := st.chat_input("Enter your question..."):
87 | st.session_state["messages"].append({"role": "user", "content": prompt})
88 |
89 | with chat_container:
90 | with st.chat_message("user"):
91 | st.markdown(prompt)
92 |
93 | with st.chat_message("assistant"):
94 | response_placeholder = st.empty()
95 | response = ""
96 |
97 | with st.spinner("Thinking..."):
98 | for delta in lyraios.run(prompt):
99 | response += delta # type: ignore
100 | response_placeholder.markdown(response)
101 |
102 | st.session_state["messages"].append({"role": "assistant", "content": response})
103 |
104 | def save_message(message):
105 | db = ChatMessage(content=message, timestamp=datetime.now())
106 | db.save()
107 | return {"status": "success", "settings": chat_settings.get_all()}
--------------------------------------------------------------------------------
/client/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
4 |
5 | const TOAST_LIMIT = 1;
6 | const TOAST_REMOVE_DELAY = 1000000;
7 |
8 | type ToasterToast = ToastProps & {
9 | id: string;
10 | title?: React.ReactNode;
11 | description?: React.ReactNode;
12 | action?: ToastActionElement;
13 | };
14 |
15 | const actionTypes = {
16 | ADD_TOAST: 'ADD_TOAST',
17 | UPDATE_TOAST: 'UPDATE_TOAST',
18 | DISMISS_TOAST: 'DISMISS_TOAST',
19 | REMOVE_TOAST: 'REMOVE_TOAST',
20 | } as const;
21 |
22 | let count = 0;
23 |
24 | function genId() {
25 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
26 | return count.toString();
27 | }
28 |
29 | type ActionType = typeof actionTypes;
30 |
31 | type Action =
32 | | {
33 | type: ActionType['ADD_TOAST'];
34 | toast: ToasterToast;
35 | }
36 | | {
37 | type: ActionType['UPDATE_TOAST'];
38 | toast: Partial;
39 | }
40 | | {
41 | type: ActionType['DISMISS_TOAST'];
42 | toastId?: ToasterToast['id'];
43 | }
44 | | {
45 | type: ActionType['REMOVE_TOAST'];
46 | toastId?: ToasterToast['id'];
47 | };
48 |
49 | interface State {
50 | toasts: ToasterToast[];
51 | }
52 |
53 | const toastTimeouts = new Map>();
54 |
55 | const addToRemoveQueue = (toastId: string) => {
56 | if (toastTimeouts.has(toastId)) {
57 | return;
58 | }
59 |
60 | const timeout = setTimeout(() => {
61 | toastTimeouts.delete(toastId);
62 | dispatch({
63 | type: 'REMOVE_TOAST',
64 | toastId: toastId,
65 | });
66 | }, TOAST_REMOVE_DELAY);
67 |
68 | toastTimeouts.set(toastId, timeout);
69 | };
70 |
71 | export const reducer = (state: State, action: Action): State => {
72 | switch (action.type) {
73 | case 'ADD_TOAST':
74 | return {
75 | ...state,
76 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
77 | };
78 |
79 | case 'UPDATE_TOAST':
80 | return {
81 | ...state,
82 | toasts: state.toasts.map((t) =>
83 | t.id === action.toast.id ? { ...t, ...action.toast } : t
84 | ),
85 | };
86 |
87 | case 'DISMISS_TOAST': {
88 | const { toastId } = action;
89 |
90 | // ! Side effects ! - This could be extracted into a dismissToast() action,
91 | // but I'll keep it here for simplicity
92 | if (toastId) {
93 | addToRemoveQueue(toastId);
94 | } else {
95 | state.toasts.forEach((toast) => {
96 | addToRemoveQueue(toast.id);
97 | });
98 | }
99 |
100 | return {
101 | ...state,
102 | toasts: state.toasts.map((t) =>
103 | t.id === toastId || toastId === undefined
104 | ? {
105 | ...t,
106 | open: false,
107 | }
108 | : t
109 | ),
110 | };
111 | }
112 | case 'REMOVE_TOAST':
113 | if (action.toastId === undefined) {
114 | return {
115 | ...state,
116 | toasts: [],
117 | };
118 | }
119 | return {
120 | ...state,
121 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
122 | };
123 | }
124 | };
125 |
126 | const listeners: Array<(state: State) => void> = [];
127 |
128 | let memoryState: State = { toasts: [] };
129 |
130 | function dispatch(action: Action) {
131 | memoryState = reducer(memoryState, action);
132 | listeners.forEach((listener) => {
133 | listener(memoryState);
134 | });
135 | }
136 |
137 | type Toast = Omit;
138 |
139 | function toast({ ...props }: Toast) {
140 | const id = genId();
141 |
142 | const update = (props: ToasterToast) =>
143 | dispatch({
144 | type: 'UPDATE_TOAST',
145 | toast: { ...props, id },
146 | });
147 | const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
148 |
149 | dispatch({
150 | type: 'ADD_TOAST',
151 | toast: {
152 | ...props,
153 | id,
154 | open: true,
155 | onOpenChange: (open) => {
156 | if (!open) dismiss();
157 | },
158 | },
159 | });
160 |
161 | return {
162 | id: id,
163 | dismiss,
164 | update,
165 | };
166 | }
167 |
168 | function useToast() {
169 | const [state, setState] = React.useState(memoryState);
170 |
171 | React.useEffect(() => {
172 | listeners.push(setState);
173 | return () => {
174 | const index = listeners.indexOf(setState);
175 | if (index > -1) {
176 | listeners.splice(index, 1);
177 | }
178 | };
179 | }, [state]);
180 |
181 | return {
182 | ...state,
183 | toast,
184 | dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
185 | };
186 | }
187 |
188 | export { useToast, toast };
189 |
--------------------------------------------------------------------------------
|