1
0

merge aigpt

This commit is contained in:
2025-06-02 18:24:43 +09:00
parent 6dbe630b9d
commit 6cd8014f80
16 changed files with 850 additions and 368 deletions

View File

@ -5,7 +5,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from datetime import datetime, timedelta
from app.services.atproto import AtprotoService
# from app.services.atproto import AtprotoService # Temporarily disabled
from app.core.config import settings
@ -97,58 +97,20 @@ async def get_optional_user(
return current_user
# Temporarily disabled due to atproto dependency issues
class AtprotoAuth:
"""atproto authentication handler"""
"""atproto authentication handler (mock implementation)"""
def __init__(self):
self.service = AtprotoService()
pass # self.service = AtprotoService()
async def authenticate(self, identifier: str, password: str) -> Optional[AuthUser]:
"""
Authenticate user with atproto
Args:
identifier: Handle or DID
password: App password
Returns:
AuthUser if successful
"""
try:
# Login to atproto
session = await self.service.login(identifier, password)
# Get user info from session
# The session contains the DID
if self.service.client:
did = self.service.client.did
handle = self.service.client.handle
return AuthUser(did=did, handle=handle)
return None
except Exception:
return None
"""Mock authentication - always returns test user"""
# Mock implementation for testing
if identifier and password:
return AuthUser(did="did:plc:test123", handle=identifier)
return None
async def verify_did_ownership(self, did: str, session_string: str) -> bool:
"""
Verify user owns the DID by checking session
Args:
did: DID to verify
session_string: Session string from login
Returns:
True if session is valid for DID
"""
try:
self.service.restore_session(session_string)
if self.service.client and self.service.client.did == did:
return True
return False
except Exception:
return False
"""Mock verification - always returns True for test"""
return True

View File

@ -15,7 +15,7 @@ class Settings(BaseSettings):
api_v1_prefix: str = "/api/v1"
# Database
database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/aicard"
database_url: str = "sqlite+aiosqlite:///~/.config/syui/ai/card/aicard.db"
database_url_supabase: Optional[str] = None
use_supabase: bool = False

View File

@ -1,4 +1,6 @@
"""Database base configuration"""
import os
from pathlib import Path
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings
@ -6,18 +8,36 @@ from app.core.config import settings
# Create base class for models
Base = declarative_base()
# Ensure database directory exists
db_path = Path.home() / ".config" / "syui" / "ai" / "card"
db_path.mkdir(parents=True, exist_ok=True)
# Select database URL based on configuration
database_url = settings.database_url_supabase if settings.use_supabase else settings.database_url
# Create async engine
engine = create_async_engine(
database_url,
echo=settings.debug,
future=True,
pool_pre_ping=True, # Enable connection health checks
pool_size=5,
max_overflow=10
)
# Expand ~ in database URL
if database_url.startswith("sqlite"):
database_url = database_url.replace("~", str(Path.home()))
# Create async engine (SQLite-optimized settings)
if "sqlite" in database_url:
engine = create_async_engine(
database_url,
echo=settings.debug,
future=True,
# SQLite-specific optimizations
connect_args={"check_same_thread": False}
)
else:
# PostgreSQL settings (fallback)
engine = create_async_engine(
database_url,
echo=settings.debug,
future=True,
pool_pre_ping=True,
pool_size=5,
max_overflow=10
)
# Create async session factory
async_session = async_sessionmaker(
@ -27,14 +47,7 @@ async_session = async_sessionmaker(
)
async def get_db() -> AsyncSession:
async def get_session() -> AsyncSession:
"""Dependency to get database session"""
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
yield session

View File

@ -1,17 +1,24 @@
"""FastAPI application entry point"""
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.routes import cards, auth, sync
from app.mcp_server import AICardMcpServer
# Create FastAPI app
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
docs_url="/docs",
redoc_url="/redoc",
)
# Initialize MCP server
enable_mcp = os.getenv("ENABLE_MCP", "true").lower() == "true"
mcp_server = AICardMcpServer(enable_mcp=enable_mcp)
# Get FastAPI app from MCP server
app = mcp_server.get_app()
# Update app configuration
app.title = settings.app_name
app.version = settings.app_version
app.docs_url = "/docs"
app.redoc_url = "/redoc"
# CORS middleware
app.add_middleware(
@ -41,7 +48,11 @@ async def root():
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy"}
return {
"status": "healthy",
"mcp_enabled": enable_mcp,
"mcp_endpoint": "/mcp" if enable_mcp else None
}
if __name__ == "__main__":

286
api/app/mcp_server.py Normal file
View File

@ -0,0 +1,286 @@
"""MCP Server for ai.card system"""
from typing import Optional, List, Dict, Any
from mcp.server.fastmcp import FastMCP
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from pathlib import Path
import logging
from app.core.config import settings
from app.db.base import get_session
from app.models.card import Card, CardRarity, CardDrawResult
from app.repositories.card import CardRepository, UniqueCardRepository
from app.repositories.user import UserRepository
from app.services.gacha import GachaService
# from app.services.card_sync import CardSyncService # Temporarily disabled
logger = logging.getLogger(__name__)
class AICardMcpServer:
"""MCP Server that exposes ai.card functionality to AI assistants"""
def __init__(self, enable_mcp: bool = True):
self.enable_mcp = enable_mcp
# Create FastAPI app
self.app = FastAPI(
title="AI.Card - Card Game System",
description="MCP server for ai.card system",
version=settings.app_version
)
# Create MCP server with FastAPI app
self.server = None
if enable_mcp:
self.server = FastMCP("aicard")
self._register_mcp_tools()
def _register_mcp_tools(self):
"""Register all MCP tools"""
@self.app.get("/get_user_cards", operation_id="get_user_cards")
async def get_user_cards(
did: str,
limit: int = 10,
session: AsyncSession = Depends(get_session)
) -> List[Dict[str, Any]]:
"""Get all cards owned by a user"""
try:
user_repo = UserRepository(session)
card_repo = CardRepository(session)
# Get user
user = await user_repo.get_by_did(did)
if not user:
return []
# Get user cards
user_cards = await card_repo.get_user_cards(user.id, limit=limit)
return [
{
"id": card.card_id,
"cp": card.cp,
"status": card.status,
"skill": card.skill,
"owner_did": did,
"obtained_at": card.obtained_at.isoformat(),
"is_unique": card.is_unique,
"unique_id": str(card.unique_id) if card.unique_id else None
}
for card in user_cards
]
except Exception as e:
logger.error(f"Error getting user cards: {e}")
return []
@self.app.post("/draw_card", operation_id="draw_card")
async def draw_card(
did: str,
is_paid: bool = False,
session: AsyncSession = Depends(get_session)
) -> Dict[str, Any]:
"""Draw a new card (gacha) for user"""
try:
gacha_service = GachaService(session)
# Draw card
card, is_unique = await gacha_service.draw_card(did, is_paid)
await session.commit()
return {
"success": True,
"card": {
"id": card.id,
"cp": card.cp,
"status": card.status,
"skill": card.skill,
"owner_did": card.owner_did,
"obtained_at": card.obtained_at.isoformat(),
"is_unique": card.is_unique,
"unique_id": card.unique_id
},
"is_unique": is_unique,
"animation_type": "kira" if card.status in [CardRarity.KIRA, CardRarity.UNIQUE] else "normal"
}
except Exception as e:
logger.error(f"Error drawing card: {e}")
await session.rollback()
return {
"success": False,
"error": str(e)
}
@self.app.get("/get_card_details", operation_id="get_card_details")
async def get_card_details(
card_id: int,
session: AsyncSession = Depends(get_session)
) -> Dict[str, Any]:
"""Get detailed information about a card type"""
try:
# Get card info from gacha service
gacha_service = GachaService(session)
if card_id not in gacha_service.CARD_INFO:
return {"error": f"Card ID {card_id} not found"}
card_info = gacha_service.CARD_INFO[card_id]
# Get unique card availability
unique_repo = UniqueCardRepository(session)
is_unique_available = await unique_repo.is_card_available(card_id)
return {
"id": card_id,
"name": card_info["name"],
"base_cp_range": card_info["base_cp_range"],
"is_unique_available": is_unique_available,
"description": f"Card {card_id}: {card_info['name']}"
}
except Exception as e:
logger.error(f"Error getting card details: {e}")
return {"error": str(e)}
@self.app.post("/sync_cards_atproto", operation_id="sync_cards_atproto")
async def sync_cards_atproto(
did: str,
session: AsyncSession = Depends(get_session)
) -> Dict[str, str]:
"""Sync user's cards with atproto (temporarily disabled)"""
return {"status": "atproto sync temporarily disabled due to dependency issues"}
@self.app.get("/analyze_card_collection", operation_id="analyze_card_collection")
async def analyze_card_collection(
did: str,
session: AsyncSession = Depends(get_session)
) -> Dict[str, Any]:
"""Analyze user's card collection"""
try:
user_repo = UserRepository(session)
card_repo = CardRepository(session)
# Get user
user = await user_repo.get_by_did(did)
if not user:
return {
"total_cards": 0,
"rarity_distribution": {},
"message": "User not found"
}
# Get all user cards
user_cards = await card_repo.get_user_cards(user.id, limit=1000)
if not user_cards:
return {
"total_cards": 0,
"rarity_distribution": {},
"message": "No cards found"
}
# Analyze collection
rarity_count = {}
total_cp = 0
card_type_count = {}
for card in user_cards:
# Rarity distribution
rarity = card.status
rarity_count[rarity] = rarity_count.get(rarity, 0) + 1
# Total CP
total_cp += card.cp
# Card type distribution
card_type_count[card.card_id] = card_type_count.get(card.card_id, 0) + 1
# Find strongest card
strongest_card = max(user_cards, key=lambda x: x.cp)
return {
"total_cards": len(user_cards),
"rarity_distribution": rarity_count,
"card_type_distribution": card_type_count,
"average_cp": total_cp / len(user_cards) if user_cards else 0,
"total_cp": total_cp,
"strongest_card": {
"id": strongest_card.card_id,
"cp": strongest_card.cp,
"status": strongest_card.status,
"is_unique": strongest_card.is_unique
},
"unique_count": len([c for c in user_cards if c.is_unique])
}
except Exception as e:
logger.error(f"Error analyzing collection: {e}")
return {"error": str(e)}
@self.app.get("/get_unique_registry", operation_id="get_unique_registry")
async def get_unique_registry(
session: AsyncSession = Depends(get_session)
) -> Dict[str, Any]:
"""Get all registered unique cards"""
try:
unique_repo = UniqueCardRepository(session)
# Get all unique cards
unique_cards = await unique_repo.get_all_unique_cards()
# Get available unique card IDs
available_ids = await unique_repo.get_available_unique_cards()
return {
"registered_unique_cards": [
{
"card_id": card.card_id,
"unique_id": card.unique_id,
"owner_did": card.owner_did,
"obtained_at": card.obtained_at.isoformat()
}
for card in unique_cards
],
"available_unique_card_ids": available_ids,
"total_registered": len(unique_cards),
"total_available": len(available_ids)
}
except Exception as e:
logger.error(f"Error getting unique registry: {e}")
return {"error": str(e)}
@self.app.get("/get_gacha_stats", operation_id="get_gacha_stats")
async def get_gacha_stats(
session: AsyncSession = Depends(get_session)
) -> Dict[str, Any]:
"""Get gacha system statistics"""
try:
return {
"rarity_probabilities": {
"normal": f"{100 - settings.prob_rare}%",
"rare": f"{settings.prob_rare - settings.prob_super_rare}%",
"super_rare": f"{settings.prob_super_rare - settings.prob_kira}%",
"kira": f"{settings.prob_kira - settings.prob_unique}%",
"unique": f"{settings.prob_unique}%"
},
"total_card_types": 16,
"card_names": [info["name"] for info in GachaService.CARD_INFO.values()],
"system_info": {
"daily_limit": "1 free draw per day",
"paid_gacha": "Enhanced probabilities",
"unique_system": "First-come-first-served globally unique cards"
}
}
except Exception as e:
logger.error(f"Error getting gacha stats: {e}")
return {"error": str(e)}
# MCP server will be run separately, not here
def get_server(self) -> Optional[FastMCP]:
"""Get the FastAPI MCP server instance"""
return self.server
def get_app(self) -> FastAPI:
"""Get the FastAPI app instance"""
return self.app

View File

@ -1,5 +1,6 @@
"""Authentication routes"""
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
@ -12,9 +13,9 @@ from app.auth.dependencies import (
AuthUser,
ACCESS_TOKEN_EXPIRE_MINUTES
)
from app.db.base import get_db
from app.db.base import get_session
from app.repositories.user import UserRepository
from app.services.atproto import AtprotoService
# from app.services.atproto import AtprotoService
router = APIRouter(prefix="/auth", tags=["auth"])
@ -45,7 +46,7 @@ class VerifyResponse(BaseModel):
async def login(
request: LoginRequest,
response: Response,
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
Login with atproto credentials

View File

@ -7,7 +7,7 @@ from app.models.card import Card, CardDraw, CardDrawResult
from app.services.gacha import GachaService
from app.repositories.user import UserRepository
from app.repositories.card import CardRepository, UniqueCardRepository
from app.db.base import get_db
from app.db.base import get_session
router = APIRouter(prefix="/cards", tags=["cards"])
@ -15,7 +15,7 @@ router = APIRouter(prefix="/cards", tags=["cards"])
@router.post("/draw", response_model=CardDrawResult)
async def draw_card(
draw_request: CardDraw,
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
カードを抽選する
@ -65,7 +65,7 @@ async def get_user_cards(
user_did: str,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
ユーザーの所有カード一覧を取得
@ -100,7 +100,7 @@ async def get_user_cards(
@router.get("/unique")
async def get_unique_cards(db: AsyncSession = Depends(get_db)):
async def get_unique_cards(db: AsyncSession = Depends(get_session)):
"""
全てのuniqueカード一覧を取得所有者情報付き
"""

View File

@ -1,10 +1,11 @@
"""Synchronization routes for atproto"""
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from app.auth.dependencies import require_user, AuthUser
from app.db.base import get_db
from app.db.base import get_session
from app.services.card_sync import CardSyncService
from app.repositories.user import UserRepository
@ -28,7 +29,7 @@ class SyncResponse(BaseModel):
async def sync_cards(
request: SyncRequest,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
Sync cards between database and atproto PDS
@ -68,7 +69,7 @@ async def sync_cards(
async def export_to_pds(
request: SyncRequest,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
Export all cards to atproto PDS
@ -97,7 +98,7 @@ async def export_to_pds(
async def import_from_pds(
request: SyncRequest,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
Import cards from atproto PDS
@ -125,7 +126,7 @@ async def verify_card_ownership(
card_id: int,
unique_id: Optional[str] = None,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_session)
):
"""
Verify user owns a specific card

View File

@ -3,7 +3,7 @@ from typing import List, Dict, Any, Optional
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.atproto import AtprotoService, CardLexicon
# from app.services.atproto import AtprotoService, CardLexicon
from app.repositories.card import CardRepository
from app.repositories.user import UserRepository
from app.models.card import Card as CardModel

View File

@ -1,5 +1,6 @@
"""Initialize database with master data"""
import asyncio
from sqlalchemy import text, select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import engine, Base, async_session
from app.db.models import CardMaster
@ -35,10 +36,14 @@ async def init_db():
print("Inserting master data...")
async with async_session() as session:
# Check if master data already exists
existing = await session.execute(
"SELECT COUNT(*) FROM card_master"
)
count = existing.scalar()
try:
result = await session.execute(
select(func.count()).select_from(CardMaster)
)
count = result.scalar()
except Exception:
# Table might not exist yet
count = 0
if count == 0:
# Insert card master data

View File

@ -1,18 +1,20 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
pydantic-settings==2.1.0
fastapi>=0.104.1
uvicorn[standard]>=0.24.0
pydantic>=2.7.0,<3.0.0
pydantic-settings>=2.1.0
python-multipart==0.0.6
httpx==0.25.2
httpx>=0.25.0,<0.29.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
sqlalchemy==2.0.23
alembic==1.12.1
asyncpg==0.29.0
psycopg2-binary==2.9.9
aiosqlite==0.19.0
sqlalchemy>=2.0.23
greenlet>=3.0.0
alembic>=1.12.1
# asyncpg==0.29.0 # Disabled: requires compilation
# psycopg2-binary==2.9.9 # Disabled: requires compilation
aiosqlite>=0.19.0
python-dotenv==1.0.0
pytest==7.4.3
pytest-asyncio==0.21.1
atproto==0.0.46
supabase==2.3.0
atproto>=0.0.55
# supabase>=2.3.0 # Temporarily disabled due to httpx version conflict
fastapi-mcp==0.1.0