袁聪的第一次提交,包含nl2sql,行情数据,场外申购

This commit is contained in:
2026-09-10 09:23:22 +08:00
parent 46fc976b24
commit 5907fcd6d2
49 changed files with 6801 additions and 33 deletions
+38
View File
@@ -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")
+110
View File
@@ -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]
+53
View File
@@ -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)
+48
View File
@@ -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]