Add card MCP tools integration and fix ServiceClient methods
### MCP Server Enhancement: - Add 3 new card-related MCP tools: get_user_cards, draw_card, get_draw_status - Fix ServiceClient missing methods for ai.card API integration - Total MCP tools now: 20 (including card functionality) ### ServiceClient Fixes: - Add get_user_cards() method for card collection retrieval - Add draw_card() method for gacha functionality - Fix JSON Value handling in card count display ### Integration Success: - ai.gpt MCP server successfully starts with all 20 tools - HTTP endpoints properly handle card-related requests - Ready for ai.card server connection on port 8000 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
390
src/openai_provider.rs
Normal file
390
src/openai_provider.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use anyhow::Result;
|
||||
use async_openai::{
|
||||
types::{
|
||||
ChatCompletionRequestMessage,
|
||||
CreateChatCompletionRequestArgs, ChatCompletionTool, ChatCompletionToolType,
|
||||
FunctionObject, ChatCompletionRequestToolMessage,
|
||||
ChatCompletionRequestAssistantMessage, ChatCompletionRequestUserMessage,
|
||||
ChatCompletionRequestSystemMessage, ChatCompletionToolChoiceOption
|
||||
},
|
||||
Client,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::http_client::ServiceClient;
|
||||
|
||||
/// OpenAI provider with MCP tools support (matching Python implementation)
|
||||
pub struct OpenAIProvider {
|
||||
client: Client<async_openai::config::OpenAIConfig>,
|
||||
model: String,
|
||||
service_client: ServiceClient,
|
||||
system_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl OpenAIProvider {
|
||||
pub fn new(api_key: String, model: Option<String>) -> Self {
|
||||
let config = async_openai::config::OpenAIConfig::new()
|
||||
.with_api_key(api_key);
|
||||
let client = Client::with_config(config);
|
||||
|
||||
Self {
|
||||
client,
|
||||
model: model.unwrap_or_else(|| "gpt-4".to_string()),
|
||||
service_client: ServiceClient::new(),
|
||||
system_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_system_prompt(mut self, prompt: String) -> Self {
|
||||
self.system_prompt = Some(prompt);
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate OpenAI tools from MCP endpoints (matching Python implementation)
|
||||
fn get_mcp_tools(&self) -> Vec<ChatCompletionTool> {
|
||||
let tools = vec![
|
||||
// Memory tools
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "get_memories".to_string(),
|
||||
description: Some("過去の会話記憶を取得します。「覚えている」「前回」「以前」などの質問で必ず使用してください".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "取得する記憶の数",
|
||||
"default": 5
|
||||
}
|
||||
}
|
||||
})),
|
||||
},
|
||||
},
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "search_memories".to_string(),
|
||||
description: Some("特定のトピックについて話した記憶を検索します。「プログラミングについて」「○○について話した」などの質問で使用してください".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keywords": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "検索キーワードの配列"
|
||||
}
|
||||
},
|
||||
"required": ["keywords"]
|
||||
})),
|
||||
},
|
||||
},
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "get_contextual_memories".to_string(),
|
||||
description: Some("クエリに関連する文脈的記憶を取得します".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "検索クエリ"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "取得する記憶の数",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
})),
|
||||
},
|
||||
},
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "get_relationship".to_string(),
|
||||
description: Some("特定ユーザーとの関係性情報を取得します".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "ユーザーID"
|
||||
}
|
||||
},
|
||||
"required": ["user_id"]
|
||||
})),
|
||||
},
|
||||
},
|
||||
// ai.card tools
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "card_get_user_cards".to_string(),
|
||||
description: Some("ユーザーが所有するカードの一覧を取得します".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"did": {
|
||||
"type": "string",
|
||||
"description": "ユーザーのDID"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "取得するカード数の上限",
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
"required": ["did"]
|
||||
})),
|
||||
},
|
||||
},
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "card_draw_card".to_string(),
|
||||
description: Some("ガチャを引いてカードを取得します".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"did": {
|
||||
"type": "string",
|
||||
"description": "ユーザーのDID"
|
||||
},
|
||||
"is_paid": {
|
||||
"type": "boolean",
|
||||
"description": "有料ガチャかどうか",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["did"]
|
||||
})),
|
||||
},
|
||||
},
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "card_analyze_collection".to_string(),
|
||||
description: Some("ユーザーのカードコレクションを分析します".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"did": {
|
||||
"type": "string",
|
||||
"description": "ユーザーのDID"
|
||||
}
|
||||
},
|
||||
"required": ["did"]
|
||||
})),
|
||||
},
|
||||
},
|
||||
ChatCompletionTool {
|
||||
r#type: ChatCompletionToolType::Function,
|
||||
function: FunctionObject {
|
||||
name: "card_get_gacha_stats".to_string(),
|
||||
description: Some("ガチャの統計情報を取得します".to_string()),
|
||||
parameters: Some(json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
})),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
/// Chat interface with MCP function calling support (matching Python implementation)
|
||||
pub async fn chat_with_mcp(&self, prompt: String, user_id: String) -> Result<String> {
|
||||
let tools = self.get_mcp_tools();
|
||||
|
||||
let system_content = self.system_prompt.as_deref().unwrap_or(
|
||||
"あなたは記憶システムと関係性データ、カードゲームシステムにアクセスできるAIです。\n\n【重要】以下の場合は必ずツールを使用してください:\n\n1. カード関連の質問:\n- 「カード」「コレクション」「ガチャ」「見せて」「持っている」「状況」「どんなカード」などのキーワードがある場合\n- card_get_user_cardsツールを使用してユーザーのカード情報を取得\n\n2. 記憶・関係性の質問:\n- 「覚えている」「前回」「以前」「について話した」「関係」などのキーワードがある場合\n- 適切なメモリツールを使用\n\n3. パラメータの設定:\n- didパラメータには現在会話しているユーザーのID(例:'syui')を使用\n- ツールを積極的に使用して正確な情報を提供してください\n\nユーザーが何かを尋ねた時は、まず関連するツールがあるかを考え、適切なツールを使用してから回答してください。"
|
||||
);
|
||||
|
||||
let request = CreateChatCompletionRequestArgs::default()
|
||||
.model(&self.model)
|
||||
.messages(vec![
|
||||
ChatCompletionRequestMessage::System(
|
||||
ChatCompletionRequestSystemMessage {
|
||||
content: system_content.to_string().into(),
|
||||
name: None,
|
||||
}
|
||||
),
|
||||
ChatCompletionRequestMessage::User(
|
||||
ChatCompletionRequestUserMessage {
|
||||
content: prompt.clone().into(),
|
||||
name: None,
|
||||
}
|
||||
),
|
||||
])
|
||||
.tools(tools)
|
||||
.tool_choice(ChatCompletionToolChoiceOption::Auto)
|
||||
.max_tokens(2000u16)
|
||||
.temperature(0.7)
|
||||
.build()?;
|
||||
|
||||
let response = self.client.chat().create(request).await?;
|
||||
let message = &response.choices[0].message;
|
||||
|
||||
// Handle tool calls
|
||||
if let Some(tool_calls) = &message.tool_calls {
|
||||
if tool_calls.is_empty() {
|
||||
println!("🔧 [OpenAI] No tools called");
|
||||
} else {
|
||||
println!("🔧 [OpenAI] {} tools called:", tool_calls.len());
|
||||
for tc in tool_calls {
|
||||
println!(" - {}({})", tc.function.name, tc.function.arguments);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("🔧 [OpenAI] No tools called");
|
||||
}
|
||||
|
||||
// Process tool calls if any
|
||||
if let Some(tool_calls) = &message.tool_calls {
|
||||
if !tool_calls.is_empty() {
|
||||
|
||||
let mut messages = vec![
|
||||
ChatCompletionRequestMessage::System(
|
||||
ChatCompletionRequestSystemMessage {
|
||||
content: system_content.to_string().into(),
|
||||
name: None,
|
||||
}
|
||||
),
|
||||
ChatCompletionRequestMessage::User(
|
||||
ChatCompletionRequestUserMessage {
|
||||
content: prompt.into(),
|
||||
name: None,
|
||||
}
|
||||
),
|
||||
ChatCompletionRequestMessage::Assistant(
|
||||
ChatCompletionRequestAssistantMessage {
|
||||
content: message.content.clone(),
|
||||
name: None,
|
||||
tool_calls: message.tool_calls.clone(),
|
||||
function_call: None,
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
// Execute each tool call
|
||||
for tool_call in tool_calls {
|
||||
println!("🌐 [MCP] Executing {}...", tool_call.function.name);
|
||||
let tool_result = self.execute_mcp_tool(tool_call, &user_id).await?;
|
||||
let result_preview = serde_json::to_string(&tool_result)?;
|
||||
let preview = if result_preview.chars().count() > 100 {
|
||||
format!("{}...", result_preview.chars().take(100).collect::<String>())
|
||||
} else {
|
||||
result_preview.clone()
|
||||
};
|
||||
println!("✅ [MCP] Result: {}", preview);
|
||||
|
||||
messages.push(ChatCompletionRequestMessage::Tool(
|
||||
ChatCompletionRequestToolMessage {
|
||||
content: serde_json::to_string(&tool_result)?,
|
||||
tool_call_id: tool_call.id.clone(),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// Get final response with tool outputs
|
||||
let final_request = CreateChatCompletionRequestArgs::default()
|
||||
.model(&self.model)
|
||||
.messages(messages)
|
||||
.max_tokens(2000u16)
|
||||
.temperature(0.7)
|
||||
.build()?;
|
||||
|
||||
let final_response = self.client.chat().create(final_request).await?;
|
||||
Ok(final_response.choices[0].message.content.as_ref().unwrap_or(&"".to_string()).clone())
|
||||
} else {
|
||||
// No tools were called
|
||||
Ok(message.content.as_ref().unwrap_or(&"".to_string()).clone())
|
||||
}
|
||||
} else {
|
||||
// No tool_calls field at all
|
||||
Ok(message.content.as_ref().unwrap_or(&"".to_string()).clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute MCP tool call (matching Python implementation)
|
||||
async fn execute_mcp_tool(&self, tool_call: &async_openai::types::ChatCompletionMessageToolCall, context_user_id: &str) -> Result<Value> {
|
||||
let function_name = &tool_call.function.name;
|
||||
let arguments: Value = serde_json::from_str(&tool_call.function.arguments)?;
|
||||
|
||||
match function_name.as_str() {
|
||||
"get_memories" => {
|
||||
let limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||
// TODO: Implement actual MCP call
|
||||
Ok(json!({"info": "記憶機能は実装中です"}))
|
||||
}
|
||||
"search_memories" => {
|
||||
let _keywords = arguments.get("keywords").and_then(|v| v.as_array());
|
||||
// TODO: Implement actual MCP call
|
||||
Ok(json!({"info": "記憶検索機能は実装中です"}))
|
||||
}
|
||||
"get_contextual_memories" => {
|
||||
let _query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let _limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
|
||||
// TODO: Implement actual MCP call
|
||||
Ok(json!({"info": "文脈記憶機能は実装中です"}))
|
||||
}
|
||||
"get_relationship" => {
|
||||
let _user_id = arguments.get("user_id").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||
// TODO: Implement actual MCP call
|
||||
Ok(json!({"info": "関係性機能は実装中です"}))
|
||||
}
|
||||
// ai.card tools
|
||||
"card_get_user_cards" => {
|
||||
let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||
let _limit = arguments.get("limit").and_then(|v| v.as_i64()).unwrap_or(10);
|
||||
|
||||
match self.service_client.get_user_cards(did).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => {
|
||||
println!("❌ ai.card API error: {}", e);
|
||||
Ok(json!({
|
||||
"error": "ai.cardサーバーが起動していません",
|
||||
"message": "カードシステムを使用するには、ai.cardサーバーを起動してください"
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
"card_draw_card" => {
|
||||
let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||
let is_paid = arguments.get("is_paid").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
|
||||
match self.service_client.draw_card(did, is_paid).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => {
|
||||
println!("❌ ai.card API error: {}", e);
|
||||
Ok(json!({
|
||||
"error": "ai.cardサーバーが起動していません",
|
||||
"message": "カードシステムを使用するには、ai.cardサーバーを起動してください"
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
"card_analyze_collection" => {
|
||||
let did = arguments.get("did").and_then(|v| v.as_str()).unwrap_or(context_user_id);
|
||||
// TODO: Implement collection analysis endpoint
|
||||
Ok(json!({
|
||||
"info": "コレクション分析機能は実装中です",
|
||||
"user_did": did
|
||||
}))
|
||||
}
|
||||
"card_get_gacha_stats" => {
|
||||
// TODO: Implement gacha stats endpoint
|
||||
Ok(json!({"info": "ガチャ統計機能は実装中です"}))
|
||||
}
|
||||
_ => {
|
||||
Ok(json!({
|
||||
"error": format!("Unknown tool: {}", function_name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user