袁聪的第一次提交,包含nl2sql,行情数据,场外申购
This commit is contained in:
@@ -18,6 +18,7 @@ class Settings(BaseSettings):
|
||||
mysql_dsn: str
|
||||
mysql_pool_size: int = Field(default=5, ge=1)
|
||||
mysql_max_overflow: int = Field(default=10, ge=0)
|
||||
mysql_pool_pre_ping: bool = False
|
||||
mysql_tx_isolation: str = "READ COMMITTED"
|
||||
redis_url: str
|
||||
redis_connect_timeout_seconds: float = Field(default=2, gt=0)
|
||||
@@ -41,6 +42,43 @@ class Settings(BaseSettings):
|
||||
worker_lease_seconds: int = Field(default=60, gt=0)
|
||||
worker_retry_limit: int = Field(default=3, ge=0)
|
||||
worker_poll_seconds: float = Field(default=1, gt=0)
|
||||
offsite_mailbox: str = "15273589815@163.com"
|
||||
offsite_allowed_senders: tuple[str, ...] = ("15008108550@163.com",)
|
||||
offsite_risk_receiver_id: str = ""
|
||||
offsite_settlement_receiver_id: str = ""
|
||||
offsite_mail_return_receiver: str = "15008108550@163.com"
|
||||
offsite_max_retry_count: int = Field(default=3, ge=0)
|
||||
offsite_imap_enabled: bool = False
|
||||
offsite_imap_host: str = ""
|
||||
offsite_imap_port: int = Field(default=993, gt=0)
|
||||
offsite_imap_username: str = ""
|
||||
offsite_imap_password: str = ""
|
||||
offsite_imap_use_ssl: bool = True
|
||||
offsite_imap_idle_enabled: bool = False
|
||||
offsite_mail_worker_enabled: bool = False
|
||||
offsite_mail_worker_batch_size: int = Field(default=20, ge=1, le=100)
|
||||
offsite_worker_user_id: str = ""
|
||||
offsite_notification_sending_timeout_seconds: int = Field(default=900, gt=0)
|
||||
offsite_mail_storage_dir: str = "data/offsite_mail"
|
||||
offsite_ocr_enabled: bool = False
|
||||
offsite_ocr_timeout_seconds: float = Field(default=30, gt=0)
|
||||
offsite_aliyun_ocr_endpoint: str = ""
|
||||
offsite_aliyun_access_key_id: str = ""
|
||||
offsite_aliyun_access_key_secret: str = ""
|
||||
offsite_deepseek_enabled: bool = False
|
||||
offsite_deepseek_base_url: str = "https://api.deepseek.com"
|
||||
offsite_deepseek_api_key: str = ""
|
||||
offsite_deepseek_model: str = "deepseek-v4-flash"
|
||||
offsite_deepseek_timeout_seconds: float = Field(default=30, gt=0)
|
||||
offsite_smtp_enabled: bool = False
|
||||
offsite_smtp_dry_run: bool = True
|
||||
offsite_smtp_host: str = ""
|
||||
offsite_smtp_port: int = Field(default=465, gt=0)
|
||||
offsite_smtp_username: str = ""
|
||||
offsite_smtp_password: str = ""
|
||||
offsite_smtp_sender: str = ""
|
||||
offsite_smtp_use_ssl: bool = True
|
||||
offsite_smtp_timeout_seconds: float = Field(default=30, gt=0)
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import re
|
||||
|
||||
DOMAINS: dict[str, set[str]] = {
|
||||
"market_nav": {"fin_market_price", "fin_nav_history", "fin_product"},
|
||||
"customer_risk": {
|
||||
"sys_customer_assignment", "fin_customer_profile", "fin_risk_assessment",
|
||||
},
|
||||
"product_fee": {"fin_product", "fin_fee_rule"},
|
||||
"trading_account": {
|
||||
"fin_sim_order", "fin_transaction", "fin_sim_account", "fin_cash_ledger",
|
||||
"fin_holding", "fin_product",
|
||||
},
|
||||
"client_content": {"client_facing_content", "fin_customer_profile"},
|
||||
}
|
||||
|
||||
TABLE_COLUMNS: dict[str, set[str]] = {
|
||||
"sys_customer_assignment": {
|
||||
"id", "customer_id", "employee_id", "employee_role", "assigned_at", "unassigned_at",
|
||||
},
|
||||
"fin_customer_profile": {
|
||||
"customer_id", "trade_account", "real_name", "investor_type", "total_asset",
|
||||
"behavior_score", "risk_tags", "opened_at", "updated_at",
|
||||
},
|
||||
"fin_risk_assessment": {
|
||||
"id", "customer_id", "total_score", "investor_type", "assessed_at",
|
||||
"valid_until", "created_at",
|
||||
},
|
||||
"fin_product": {
|
||||
"id", "product_code", "product_name", "exchange_code", "product_category",
|
||||
"risk_level", "current_nav", "transaction_fee_rate", "status",
|
||||
"created_at", "updated_at",
|
||||
},
|
||||
"fin_fee_rule": {
|
||||
"id", "rule_code", "product_id", "exchange_code", "order_side", "customer_tier",
|
||||
"fee_rate", "minimum_fee", "fixed_fee", "effective_from", "effective_until",
|
||||
"status", "created_at", "updated_at",
|
||||
},
|
||||
"fin_market_price": {
|
||||
"id", "product_id", "trade_date", "open_price", "high_price", "low_price",
|
||||
"close_price", "volume", "turnover_amount", "created_at",
|
||||
},
|
||||
"fin_nav_history": {"id", "product_id", "nav_date", "nav", "created_at"},
|
||||
"fin_holding": {
|
||||
"id", "customer_id", "trade_account", "product_id", "total_quantity",
|
||||
"available_quantity", "frozen_quantity", "market_value", "profit_loss",
|
||||
"profit_loss_ratio", "status", "updated_at",
|
||||
},
|
||||
"fin_transaction": {
|
||||
"id", "transaction_no", "order_id", "customer_id", "account_id", "product_id",
|
||||
"order_side", "executed_price", "executed_quantity", "gross_amount",
|
||||
"fee_amount", "net_amount", "quote_at", "executed_at", "created_at",
|
||||
},
|
||||
"fin_sim_order": {
|
||||
"id", "order_no", "customer_id", "account_id", "product_id", "order_side",
|
||||
"price_type", "quantity", "quote_price", "quote_at", "advisor_id",
|
||||
"filled_quantity", "status", "submitted_at", "cancelled_at", "created_at",
|
||||
"updated_at",
|
||||
},
|
||||
"fin_sim_account": {
|
||||
"id", "account_no", "customer_id", "currency", "cash_balance", "available_cash",
|
||||
"frozen_cash", "initial_balance", "status", "version", "created_at", "updated_at",
|
||||
},
|
||||
"fin_cash_ledger": {
|
||||
"id", "ledger_no", "account_id", "transaction_id", "entry_type", "amount",
|
||||
"balance_after", "available_cash_after", "frozen_cash_after", "occurred_at",
|
||||
"created_at",
|
||||
},
|
||||
"client_facing_content": {
|
||||
"id", "customer_id", "content_type", "draft_content", "review_status",
|
||||
"reviewer_user_id", "reviewed_at", "published_at", "created_at", "updated_at",
|
||||
},
|
||||
}
|
||||
|
||||
ALLOWED_TABLES = set(TABLE_COLUMNS)
|
||||
CURRENT_ONLY_TABLES = {"fin_holding", "fin_sim_account", "sys_customer_assignment"}
|
||||
BANNED_SQL = re.compile(
|
||||
r"\b(INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE|CREATE|GRANT|REVOKE|CALL|EXEC|MERGE)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
ALIASES = {
|
||||
"sys_customer_assignment": "ca", "fin_customer_profile": "cp",
|
||||
"fin_risk_assessment": "ra", "fin_product": "p", "fin_fee_rule": "f",
|
||||
"fin_market_price": "m", "fin_nav_history": "n", "fin_holding": "h",
|
||||
"fin_transaction": "t", "fin_sim_order": "o", "fin_sim_account": "a",
|
||||
"fin_cash_ledger": "l", "client_facing_content": "c",
|
||||
}
|
||||
|
||||
JOIN_SQL = {
|
||||
("fin_market_price", "fin_product"): "m.product_id = p.id",
|
||||
("fin_nav_history", "fin_product"): "n.product_id = p.id",
|
||||
("fin_fee_rule", "fin_product"): "f.product_id = p.id",
|
||||
("fin_holding", "fin_product"): "h.product_id = p.id",
|
||||
("fin_holding", "fin_customer_profile"): "h.customer_id = cp.customer_id",
|
||||
("fin_transaction", "fin_product"): "t.product_id = p.id",
|
||||
("fin_transaction", "fin_customer_profile"): "t.customer_id = cp.customer_id",
|
||||
("fin_transaction", "fin_sim_account"): "t.account_id = a.id",
|
||||
("fin_sim_order", "fin_product"): "o.product_id = p.id",
|
||||
("fin_sim_order", "fin_customer_profile"): "o.customer_id = cp.customer_id",
|
||||
("fin_sim_order", "fin_sim_account"): "o.account_id = a.id",
|
||||
("fin_sim_account", "fin_customer_profile"): "a.customer_id = cp.customer_id",
|
||||
("fin_cash_ledger", "fin_sim_account"): "l.account_id = a.id",
|
||||
("fin_risk_assessment", "fin_customer_profile"): "ra.customer_id = cp.customer_id",
|
||||
("sys_customer_assignment", "fin_customer_profile"): "ca.customer_id = cp.customer_id",
|
||||
("client_facing_content", "fin_customer_profile"): "c.customer_id = cp.customer_id",
|
||||
}
|
||||
|
||||
|
||||
def alias(table: str) -> str:
|
||||
return ALIASES[table]
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class FinancialNL2SQLInput(BaseModel):
|
||||
"""金融 NL2SQL 公共工具入参;身份和权限只信任 RequestContext。"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
question: str = Field(min_length=1, max_length=500)
|
||||
confirmation: str | None = Field(default=None, max_length=200)
|
||||
dry_run: bool = False
|
||||
limit: int = Field(default=50, ge=1, le=200)
|
||||
|
||||
@field_validator("question")
|
||||
@classmethod
|
||||
def question_must_not_be_blank(cls, value: str) -> str:
|
||||
if not value.strip():
|
||||
raise ValueError("question must not be blank")
|
||||
return value.strip()
|
||||
|
||||
|
||||
class FinancialQueryPlan(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
intent: str
|
||||
domains: tuple[str, ...]
|
||||
tables: tuple[str, ...]
|
||||
metrics: tuple[str, ...] = ()
|
||||
dimensions: tuple[str, ...] = ()
|
||||
filters: tuple[dict[str, Any], ...] = ()
|
||||
time_mode: str = "none"
|
||||
time_column: str | None = None
|
||||
start: str | None = None
|
||||
end: str | None = None
|
||||
limit: int = Field(default=50, ge=1, le=200)
|
||||
confidence: float = Field(default=0.0, ge=0, le=1)
|
||||
needs_confirmation: bool = False
|
||||
confirmation_question: str | None = None
|
||||
unsupported_reason: str | None = None
|
||||
|
||||
|
||||
class FinancialNL2SQLResult(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
status: str
|
||||
message: str
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
query_plan: dict[str, Any] = Field(default_factory=dict)
|
||||
sql: str | None = None
|
||||
parameters: dict[str, Any] = Field(default_factory=dict)
|
||||
audit: dict[str, Any] = Field(default_factory=dict)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""场外基金申购赎回业务契约。"""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
DocumentType = Literal["summary", "subscription", "redemption", "other"]
|
||||
PlanTaskStatus = Literal["待执行", "执行中", "已完成", "查询失败", "无法判断", "不适用"]
|
||||
RuleResultStatus = Literal["正常", "异常", "无法判断"]
|
||||
OperationDecision = Literal["确认正常", "确认异常", "未处理"]
|
||||
SendStatus = Literal["待发送", "发送中", "发送成功", "发送失败"]
|
||||
|
||||
|
||||
class RecognizedAttachment(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
filename: str = Field(min_length=1, max_length=255)
|
||||
file_hash: str = Field(min_length=16, max_length=128)
|
||||
original_file_path: str = Field(default="", max_length=500)
|
||||
media_type: str = Field(default="application/octet-stream", max_length=128)
|
||||
size_bytes: int = Field(default=0, ge=0)
|
||||
document_type: DocumentType
|
||||
extracted_fields: dict[str, object] = Field(default_factory=dict)
|
||||
field_confidence: dict[str, Decimal] = Field(default_factory=dict)
|
||||
ocr_text: str = ""
|
||||
page_evidence: dict[str, object] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ReceiveRecognizedMailRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
imap_uid: str = Field(min_length=1, max_length=64)
|
||||
message_id: str = Field(min_length=1, max_length=255)
|
||||
sender: str = Field(min_length=3, max_length=255)
|
||||
return_path: str | None = Field(default=None, max_length=255)
|
||||
auth_result: dict[str, object] = Field(default_factory=dict)
|
||||
eml_path: str = Field(default="", max_length=500)
|
||||
attachments: tuple[RecognizedAttachment, ...]
|
||||
|
||||
|
||||
class OffsiteDocumentSummary(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
task_id: str
|
||||
document_type: DocumentType
|
||||
status: str
|
||||
rule_results: dict[str, RuleResultStatus]
|
||||
Reference in New Issue
Block a user