Backend (FastAPI + SQLAlchemy + Claude API + RAG): - Config management with Pydantic v2 - Database engine with connection pooling and SQL injection prevention - AI engine with Claude API integration (support custom base URL) - RAG engine with ChromaDB and sentence-transformers - Requirement analysis service - Config generation service - Executor engine with SQL validation - REST API endpoints: /analyze, /generate, /execute Frontend (Vue 3 + Element Plus + Pinia): - Complete 3-step workflow: analyze → generate → execute - Step indicator with progress visualization - Analysis result display with field table - SQL preview with monospace font - Execute confirmation dialog with safety warning - Execution result display - State management with Pinia - API service integration Security: - SQL injection prevention with parameterized queries - Dangerous SQL operation blocking - Database password URL encoding - Transaction auto-rollback - Pydantic config validation Features: - Natural language requirement analysis - Automated SQL configuration generation - Safe execution with human review - LAN access support - Custom Claude API endpoint support Documentation: - README with quick start guide - Quick start guide - LAN access configuration - Dependency fixes guide - Claude API configuration - Git operation guide - Implementation report Dependencies fixed: - numpy<2.0.0 for chromadb compatibility - sentence-transformers==2.7.0 for huggingface_hub compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
3.8 KiB
Python
121 lines
3.8 KiB
Python
"""AI Engine for ERP AI Assistant.
|
||
|
||
This module provides the ClaudeEngine class that wraps Claude API calls
|
||
and provides JSON parsing utilities.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
from typing import Any
|
||
|
||
import anthropic
|
||
from loguru import logger
|
||
|
||
from app.config import get_settings
|
||
|
||
|
||
class ClaudeEngine:
|
||
"""Engine for interacting with Claude API.
|
||
|
||
This class wraps the Anthropic Claude API client and provides
|
||
utilities for parsing JSON responses from Claude.
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
"""Initialize Claude engine with settings."""
|
||
settings = get_settings()
|
||
|
||
# Initialize Anthropic client with optional custom base_url
|
||
client_kwargs = {"api_key": settings.ANTHROPIC_API_KEY}
|
||
if settings.ANTHROPIC_BASE_URL:
|
||
client_kwargs["base_url"] = settings.ANTHROPIC_BASE_URL
|
||
logger.info(f"Using custom Anthropic base URL: {settings.ANTHROPIC_BASE_URL}")
|
||
|
||
self.client = anthropic.AsyncAnthropic(**client_kwargs)
|
||
self.model = settings.CLAUDE_MODEL
|
||
self.max_tokens = settings.CLAUDE_MAX_TOKENS
|
||
self.temperature = settings.CLAUDE_TEMPERATURE
|
||
|
||
def parse_json_response(self, content: str) -> dict[str, Any]:
|
||
"""Parse JSON from Claude responses.
|
||
|
||
Attempts multiple parsing strategies:
|
||
1. Direct JSON parse
|
||
2. Extract from markdown code blocks
|
||
3. Extract any {...} block
|
||
|
||
Args:
|
||
content: The response content from Claude
|
||
|
||
Returns:
|
||
Parsed JSON as a dictionary
|
||
|
||
Raises:
|
||
ValueError: If JSON cannot be parsed using any strategy
|
||
"""
|
||
if not content or not content.strip():
|
||
raise ValueError("Empty content provided")
|
||
|
||
# Strategy 1: Try direct JSON parse
|
||
try:
|
||
return json.loads(content)
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Strategy 2: Try extracting from markdown code blocks
|
||
json_code_block_pattern = r'```json\s*(\{.*?\})\s*```'
|
||
json_match = re.search(json_code_block_pattern, content, re.DOTALL)
|
||
if json_match:
|
||
try:
|
||
return json.loads(json_match.group(1))
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Also try any code block (not just json tagged)
|
||
code_block_pattern = r'```\s*(\{.*?\})\s*```'
|
||
code_block_match = re.search(code_block_pattern, content, re.DOTALL)
|
||
if code_block_match:
|
||
try:
|
||
return json.loads(code_block_match.group(1))
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Strategy 3: Try extracting any {...} block
|
||
# Find balanced braces
|
||
brace_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
|
||
json_blocks = re.findall(brace_pattern, content, re.DOTALL)
|
||
for json_block in json_blocks:
|
||
try:
|
||
return json.loads(json_block)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
# All strategies failed
|
||
logger.error(f"无法解析 Claude 返回的 JSON: {content[:200]}")
|
||
raise ValueError("无法解析 Claude 返回的 JSON,请检查响应格式")
|
||
|
||
async def call_claude(
|
||
self,
|
||
messages: list[dict[str, str]],
|
||
temperature: float | None = None
|
||
) -> str:
|
||
"""Call Claude API.
|
||
|
||
Args:
|
||
messages: List of message dictionaries with 'role' and 'content'
|
||
temperature: Optional temperature override (0-2)
|
||
|
||
Returns:
|
||
The text content from Claude's response
|
||
|
||
Raises:
|
||
Exception: If the API call fails
|
||
"""
|
||
response = await self.client.messages.create(
|
||
model=self.model,
|
||
max_tokens=self.max_tokens,
|
||
temperature=temperature if temperature is not None else self.temperature,
|
||
messages=messages
|
||
)
|
||
return response.content[0].text
|