feat: scaffold agentic interior design workflow
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""External service adapters."""
|
||||
@@ -0,0 +1,49 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
def _configured(value: str) -> bool:
|
||||
return bool(value) and not value.startswith("replace_with_")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderCapability:
|
||||
provider: str
|
||||
configured: bool
|
||||
strengths: tuple[str, ...]
|
||||
|
||||
|
||||
class ModelRouter:
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
def capabilities(self) -> list[ProviderCapability]:
|
||||
return [
|
||||
ProviderCapability(
|
||||
provider="seedream",
|
||||
configured=_configured(self.settings.ark_api_key)
|
||||
and bool(self.settings.seedream_model_endpoint),
|
||||
strengths=("中文风格指令", "快速方向探索"),
|
||||
),
|
||||
ProviderCapability(
|
||||
provider="openai",
|
||||
configured=_configured(self.settings.openai_api_key),
|
||||
strengths=("局部编辑", "多轮一致性", "遮罩修改"),
|
||||
),
|
||||
ProviderCapability(
|
||||
provider="gemini",
|
||||
configured=_configured(self.settings.gemini_api_key),
|
||||
strengths=("多参考图理解", "复杂视觉指令"),
|
||||
),
|
||||
]
|
||||
|
||||
def choose_image_provider(self, task: str) -> str:
|
||||
configured = {item.provider for item in self.capabilities() if item.configured}
|
||||
priorities = [item.strip() for item in self.settings.image_provider_priority.split(",")]
|
||||
if task == "masked_edit" and "openai" in configured:
|
||||
return "openai"
|
||||
for provider in priorities:
|
||||
if provider in configured:
|
||||
return provider
|
||||
raise RuntimeError("No image provider is configured.")
|
||||
@@ -0,0 +1,51 @@
|
||||
import base64
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
class BaiduOcrAdapter:
|
||||
OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic"
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(
|
||||
self.settings.baidu_ocr_enabled
|
||||
and self.settings.baidu_ocr_api_key
|
||||
and self.settings.baidu_ocr_secret_key
|
||||
)
|
||||
|
||||
async def _access_token(self) -> str:
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
response = await client.post(
|
||||
self.settings.baidu_ocr_token_url,
|
||||
params={
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": self.settings.baidu_ocr_api_key,
|
||||
"client_secret": self.settings.baidu_ocr_secret_key,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["access_token"]
|
||||
|
||||
async def recognize(self, image_bytes: bytes) -> list[dict]:
|
||||
if not self.configured:
|
||||
raise RuntimeError("Baidu OCR is not configured or enabled.")
|
||||
token = await self._access_token()
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
response = await client.post(
|
||||
self.OCR_URL,
|
||||
params={"access_token": token},
|
||||
data={
|
||||
"image": base64.b64encode(image_bytes).decode("ascii"),
|
||||
"detect_direction": "true",
|
||||
"paragraph": "false",
|
||||
},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json().get("words_result", [])
|
||||
@@ -0,0 +1,65 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PresignedUpload:
|
||||
method: str
|
||||
url: str
|
||||
object_key: str
|
||||
expires_in: int
|
||||
|
||||
|
||||
class S3Storage:
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
values = (
|
||||
self.settings.s3_endpoint,
|
||||
self.settings.s3_access_key_id,
|
||||
self.settings.s3_secret_access_key,
|
||||
)
|
||||
return all(values) and not any(value.startswith("replace_with_") for value in values)
|
||||
|
||||
def _client(self, public: bool = False):
|
||||
endpoint = (
|
||||
self.settings.s3_public_endpoint
|
||||
if public and self.settings.s3_public_endpoint
|
||||
else self.settings.s3_endpoint
|
||||
)
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint,
|
||||
aws_access_key_id=self.settings.s3_access_key_id,
|
||||
aws_secret_access_key=self.settings.s3_secret_access_key,
|
||||
region_name=self.settings.s3_region,
|
||||
config=Config(
|
||||
signature_version="s3v4",
|
||||
s3={"addressing_style": "path" if self.settings.s3_force_path_style else "auto"},
|
||||
),
|
||||
)
|
||||
|
||||
def presign_input_upload(self, object_key: str, content_type: str) -> PresignedUpload:
|
||||
if not self.configured:
|
||||
raise RuntimeError("MinIO/S3 is not configured.")
|
||||
url = self._client(public=True).generate_presigned_url(
|
||||
"put_object",
|
||||
Params={
|
||||
"Bucket": self.settings.s3_bucket_inputs,
|
||||
"Key": object_key,
|
||||
"ContentType": content_type,
|
||||
},
|
||||
ExpiresIn=self.settings.s3_presigned_url_ttl_seconds,
|
||||
)
|
||||
return PresignedUpload(
|
||||
method="PUT",
|
||||
url=url,
|
||||
object_key=object_key,
|
||||
expires_in=self.settings.s3_presigned_url_ttl_seconds,
|
||||
)
|
||||
Reference in New Issue
Block a user