Skip to content

Commit

Permalink
Onchain ai chat (#3283)
Browse files Browse the repository at this point in the history
* [examples] onchain_ai_chat example

* test python script

* Support AI room

* implement web

* style and layout

* Message page

* Room list

* fixup

* fix timestamp

* Use AI oracle

* fixup

* fixup

* fix tests

* fix up

* refactor

* fixup
  • Loading branch information
jolestar authored Feb 9, 2025
1 parent 41db52a commit cc85ecf
Show file tree
Hide file tree
Showing 44 changed files with 8,937 additions and 1 deletion.
20 changes: 20 additions & 0 deletions examples/onchain_ai_chat/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "onchain_ai_chat"
version = "0.0.1"

[dependencies]
MoveStdlib = { local = "../../frameworks/move-stdlib" }
MoveosStdlib = { local = "../../frameworks/moveos-stdlib" }
RoochFramework = { local = "../../frameworks/rooch-framework" }
BitcoinMove = { local = "../../frameworks/bitcoin-move" }
VerityMoveOracles = { git = "https://github.com/usherlabs/verity-move-oracles", subdir = "rooch", rev = "5053cb6563eca20f2c1c38784e8b20e9e3bb617f"}

[addresses]
onchain_ai_chat = "_"
verity = "0xf1290fb0e7e1de7e92e616209fb628970232e85c4c1a264858ff35092e1be231"
std = "0x1"
moveos_std = "0x2"
rooch_framework = "0x3"

[dev-addresses]
onchain_ai_chat = "0x42"
56 changes: 56 additions & 0 deletions examples/onchain_ai_chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Onchain AI Chat

A decentralized chat application built on Rooch blockchain with AI integration through Verity Oracle.

## Features

- Create and join chat rooms
- Real-time messaging with other users
- AI-powered chat rooms with GPT-4 integration
- On-chain message storage and persistence
- Pagination support for message history
- Real-time message updates

## Architecture

### Smart Contracts

- `room.move`: Main chat room functionality including message handling
- `ai_service.move`: AI integration service using Verity Oracle
- `ai_callback.move`: Handles AI response callbacks

### Frontend

- React-based web interface
- Real-time updates using Rooch SDK
- Material design UI components
- Message pagination and infinite scroll

## Prerequisites

- [Rooch](https://rooch.network) development environment
- Node.js v16+ and npm/yarn
- Move compiler

## Getting Started

1. Clone the repository:
```bash
git clone https://github.com/rooch-network/rooch.git
cd rooch/examples/onchain_ai_chat
```

2. Deploy the smart contracts:

```bash
rooch move publish --named-addresses onchain_ai_chat=default
```

3. Start the frontend:

```bash
cd web
pnpm install
pnpm dev
```
More details can be found in the [web README](web/README.md).
196 changes: 196 additions & 0 deletions examples/onchain_ai_chat/chat_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env python3

import subprocess
import json
import time
from typing import List, Optional, Dict

class RoochChatTester:
def __init__(self):
# Get accounts from rooch
self.accounts = self._get_accounts()
# Use the default account as admin, and other accounts as users
self.admin = self.accounts["default"]["hex_address"]
self.user1 = self.accounts["account0"]["hex_address"]
self.user2 = self.accounts["account1"]["hex_address"]
# Initialize gas for test accounts
self._init_account_gas()

def _init_account_gas(self):
"""Initialize gas for test accounts"""
test_accounts = ["account0", "account1"]
min_gas_amount = "1000000000" # 1 RGas

for account in test_accounts:
# Check account balance
balance_command = [
"rooch", "account", "balance",
"--address", self.accounts[account]["address"],
"--json"
]
result = self.run_command(balance_command)
if result:
# Updated balance parsing to match new format
rgas_balance = result.get("RGAS", {}).get("balance", "0")
if int(rgas_balance) < int(min_gas_amount):
print(f"Transferring gas to {account}...")
transfer_command = [
"rooch", "account", "transfer",
"--to", self.accounts[account]["address"],
"--coin-type", "0x3::gas_coin::RGas",
"--amount", min_gas_amount,
"--json"
]
self.run_command(transfer_command)
print(f"Transferred {min_gas_amount} RGas to {account}")

def _get_accounts(self) -> Dict:
"""Get accounts from rooch command"""
try:
result = subprocess.run(
["rooch", "account", "list", "--json"],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error getting accounts: {e.stderr}")
raise e
except json.JSONDecodeError as e:
print(f"Error parsing account list JSON: {e}")
raise e

def run_command(self, command: List[str]) -> Optional[dict]:
"""Run a rooch command and return the JSON output if any"""
try:
# Print the complete command
print(f"\nExecuting command: {' '.join(command)}")
result = subprocess.run(command, capture_output=True, text=True, check=True)
if result.stdout:
print(f"Command output: {result.stdout}")
if result.stdout and '{' in result.stdout:
json_result = json.loads(result.stdout)
# Check if transaction failed
if 'output' in json_result and 'status' in json_result['output']:
status = json_result['output']['status']
if status.get('type') == 'moveabort':
raise Exception(f"Transaction failed: {status}")
return json_result
return None
except subprocess.CalledProcessError as e:
print(f"Error running command: {' '.join(command)}")
print(f"Error output: {e.stderr}")
raise e

def create_room(self, account: str, title: str, is_public: bool, is_ai: bool = False) -> str:
"""Create a new chat room and return its object ID"""
# Replace spaces with underscores in title
safe_title = title.replace(" ", "_")

# Choose the appropriate entry function based on room type
entry_function = f"{self.admin}::room::create_ai_room_entry" if is_ai else f"{self.admin}::room::create_room_entry"

command = [
"rooch", "move", "run",
"--sender", account,
"--function", entry_function,
"--args", f"string:{safe_title}",
"--args", f"bool:{str(is_public).lower()}",
"--json"
]
result = self.run_command(command)
if result and 'output' in result:
changes = result['output'].get('changeset', {}).get('changes', [])
for change in changes:
metadata = change.get('metadata', {})
if metadata.get('object_type', '').endswith('::room::Room'):
return metadata.get('id')
return None

def create_ai_room(self, account: str, title: str, is_public: bool) -> str:
"""Convenience method to create an AI chat room"""
return self.create_room(account, title, is_public, True)

def send_message(self, account: str, room_id: str, message: str):
"""Send a message to a room"""
# Replace spaces with underscores in message
safe_message = message.replace(" ", "_")
command = [
"rooch", "move", "run",
"--sender", account,
"--function", f"{self.admin}::room::send_message_entry",
"--args", f"object:{room_id}",
"--args", f"string:{safe_message}",
"--json" # Add json flag to get structured output
]
self.run_command(command)

def add_member(self, account: str, room_id: str, member: str):
"""Add a member to a private room"""
command = [
"rooch", "move", "run", "--sender", account,
"--function", f"{self.admin}::room::add_member_entry",
"--args", f"object:{room_id}",
"--args", f"address:{member}",
"--json" # Add json flag to get structured output
]
self.run_command(command)

def main():
tester = RoochChatTester()

print("=== Testing Chat Room Contract ===")
print(f"Using accounts:")
print(f"Admin: {tester.admin}")
print(f"User1: {tester.user1}")
print(f"User2: {tester.user2}")

# Test 1: Create public room (normal)
print("\n1. Creating public room...")
public_room_id = tester.create_room(tester.admin, "Public_Room", True, False)
print(f"Public room created with ID: {public_room_id}")

# Test 2: Send message to public room
print("\n2. Sending message to public room...")
tester.send_message(tester.user1, public_room_id, "Hello,_public_room!")
print("Message sent successfully")

# Test 3: Create private room (normal)
print("\n3. Creating private room...")
private_room_id = tester.create_room(tester.admin, "Private_Room", False, False)
print(f"Private room created with ID: {private_room_id}")

# Test 4: Create AI room
print("\n4. Creating AI room...")
ai_room_id = tester.create_ai_room(tester.admin, "AI_Room", True)
print(f"AI room created with ID: {ai_room_id}")

# Test 5: Send message to AI room
print("\n5. Sending message to AI room...")
tester.send_message(tester.user1, ai_room_id, "Hello,_AI!")
print("Message sent successfully")

# Test 6: Add member to private room
print("\n6. Adding member to private room...")
tester.add_member(tester.admin, private_room_id, tester.user1)
print(f"Added user {tester.user1} to private room")

# Test 7: Send message to private room
print("\n7. Sending message to private room...")
tester.send_message(tester.user1, private_room_id, "Hello,_private_room!")
print("Message sent successfully")

# Test 8: Try unauthorized access (should fail)
print("\n8. Testing unauthorized access...")
try:
tester.send_message(tester.user2, private_room_id, "Unauthorized_message")
print("ERROR: Unauthorized message succeeded when it should have failed")
except Exception as e:
if "moveabort" in str(e) and "abort_code" in str(e):
print("Successfully caught unauthorized access attempt")
else:
raise e

if __name__ == "__main__":
main()
60 changes: 60 additions & 0 deletions examples/onchain_ai_chat/sources/ai_callback.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module onchain_ai_chat::ai_callback {
use moveos_std::object;
use moveos_std::string_utils;
use moveos_std::json;
use onchain_ai_chat::room::{Self, Room};
use onchain_ai_chat::ai_service;
use onchain_ai_chat::ai_response;
use verity::oracles;
use std::option;
use std::vector;
use std::string::{Self, String};

public entry fun process_response() {
let pending_requests = ai_service::get_pending_requests();

vector::for_each(pending_requests, |request| {
let (room_id, request_id) = ai_service::unpack_pending_request(request);

let response_status = oracles::get_response_status(&request_id);

if (response_status != 0) {
let response = oracles::get_response(&request_id);
let response_content = option::destroy_some(response);
let room_obj = object::borrow_mut_object_shared<Room>(room_id);
let room = object::borrow_mut(room_obj);
let message = if (response_status == 200){
let json_str_opt = json::from_json_option<String>(string::into_bytes(response_content));
let json_str = if(option::is_some(&json_str_opt)){
option::destroy_some(json_str_opt)
}else{
response_content
};
let chat_completion_opt = ai_response::parse_chat_completion_option(json_str);
if(option::is_some(&chat_completion_opt)){
let chat_completion = option::destroy_some(chat_completion_opt);
let message_content = ai_response::get_message_content(&chat_completion);
let refusal = ai_response::get_refusal(&chat_completion);
if(option::is_some(&refusal)){
let refusal_reason = option::destroy_some(refusal);
string::append(&mut message_content, string::utf8(b", refusal: "));
string::append(&mut message_content, refusal_reason);
};
message_content
}else{
response_content
}
}else{
let error_message = string::utf8(b"AI Oracle response error, error code: ");
string::append(&mut error_message, string_utils::to_string_u32((response_status as u32)));
string::append(&mut error_message, string::utf8(b", response: "));
string::append(&mut error_message, response_content);
error_message
};
room::add_ai_response(room, message);
ai_service::remove_request(request_id);
};
});

}
}
Loading

0 comments on commit cc85ecf

Please sign in to comment.