Skip to content

Commit d7be32a

Browse files
committed
fix i18n dashboard
1 parent eb73aed commit d7be32a

12 files changed

Lines changed: 673 additions & 159 deletions

File tree

.DS_Store

-6 KB
Binary file not shown.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@ test-results/
33
**/report.html
44
docker-compose
55
**/node_modules/
6+
7+
*.pyc
8+
9+
.DS_Store
10+
.env.development
11+
__pycache__/
12+
*.pyc

globalization-service/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
ENV PYTHONDONTWRITEBYTECODE=1 \
6+
PYTHONUNBUFFERED=1
7+
8+
COPY requirements.txt .
9+
RUN pip install --no-cache-dir -r requirements.txt
10+
11+
COPY app ./app
12+
13+
EXPOSE 8080
14+
15+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

globalization-service/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# OPEA Globalization & Governance Service (Topic A)
2+
3+
This service extends OPEA GenAIStudio with a simple globalization & governance layer:
4+
5+
- Per-tenant region & language configuration
6+
- Prompt preview & rewriting with language hints and hallucination notices
7+
- Very basic keyword-based content policy evaluation
8+
- Event logging endpoint for building “internationalization effectiveness” dashboard
9+
10+
## Run locally
11+
12+
```bash
13+
cd globalization-service
14+
pip install -r requirements.txt
15+
uvicorn app.main:app --host 0.0.0.0 --port 8080
16+
Open http://localhost:8080/docs to try the APIs.
17+
18+
Docker
19+
20+
docker build -t globalization-service:0.1.0 .
21+
docker run -p 8080:8080 globalization-service:0.1.0
22+
Integration idea with GenAIStudio / OPEA
23+
Call /v1/globalization/prompt/preview in your app-frontend or studio-frontend
24+
before sending user prompt to OPEA ChatQnA.
25+
26+
Call /v1/globalization/policy/evaluate in your backend to decide allow/deny.
27+
28+
Use /v1/globalization/events as a data source for a “globalization governance dashboard”.

globalization-service/app/__init__.py

Whitespace-only changes.

globalization-service/app/main.py

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
import os
2+
from datetime import datetime
3+
from typing import List, Optional, Dict, Any
4+
from uuid import uuid4
5+
6+
from fastapi import FastAPI, HTTPException
7+
from fastapi.responses import FileResponse
8+
from fastapi.staticfiles import StaticFiles
9+
from pydantic import BaseModel, Field
10+
11+
app = FastAPI(
12+
title="OPEA Globalization & Governance Service",
13+
description="Language-aware gateway for OPEA-based GenAI apps (Topic A)",
14+
version="0.1.0",
15+
)
16+
17+
# -----------------------------
18+
# 静态文件目录 & 看板页面
19+
# -----------------------------
20+
21+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22+
STATIC_DIR = os.path.join(BASE_DIR, "static")
23+
os.makedirs(STATIC_DIR, exist_ok=True)
24+
25+
# 挂载 /static,用于访问 dashboard 里的静态资源(如果后面要拆 css/js)
26+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
27+
28+
29+
@app.get("/", include_in_schema=False)
30+
@app.get("/dashboard", include_in_schema=False)
31+
def dashboard_page():
32+
"""
33+
国际化运营看板页面
34+
访问 http://localhost:8080/ 或 /dashboard 即可
35+
"""
36+
dashboard_path = os.path.join(STATIC_DIR, "dashboard.html")
37+
if not os.path.exists(dashboard_path):
38+
raise HTTPException(status_code=404, detail="dashboard.html not found")
39+
return FileResponse(dashboard_path)
40+
41+
42+
# -----------------------------
43+
# 配置 & 内存事件日志
44+
# -----------------------------
45+
46+
47+
class RegionConfig(BaseModel):
48+
region_code: str # cn / eu / us 等
49+
default_language: str # zh-CN / en-US / th-TH ...
50+
allowed_languages: List[str]
51+
blocked_keywords: List[str] # 简单敏感词示例
52+
hallucination_notice: bool # 是否强制附带“内容可能有误差”提示
53+
pii_strict: bool # 是否启用更严格的隐私限制
54+
55+
56+
TENANT_CONFIGS: Dict[str, RegionConfig] = {
57+
"demo-tenant-cn": RegionConfig(
58+
region_code="cn",
59+
default_language="zh-CN",
60+
allowed_languages=["zh-CN", "en-US", "th-TH"],
61+
blocked_keywords=["政治敏感", "违法犯罪"], # 纯示例
62+
hallucination_notice=True,
63+
pii_strict=True,
64+
),
65+
"demo-tenant-eu": RegionConfig(
66+
region_code="eu",
67+
default_language="en-US",
68+
allowed_languages=["en-US", "de-DE", "fr-FR"],
69+
blocked_keywords=["hate speech", "terrorism"],
70+
hallucination_notice=True,
71+
pii_strict=True,
72+
),
73+
}
74+
75+
# 简单事件缓存,用于看板
76+
EVENT_LOG: List[Dict[str, Any]] = []
77+
78+
79+
# -----------------------------
80+
# 模型定义
81+
# -----------------------------
82+
83+
84+
class PromptPreviewRequest(BaseModel):
85+
tenant_id: str = Field(..., description="租户 ID,例如 demo-tenant-cn")
86+
region: Optional[str] = Field(None, description="区域编码,可选,默认取租户配置")
87+
language: Optional[str] = Field(None, description="目标语言,不填则用租户默认语言")
88+
raw_prompt: str = Field(..., description="原始提示词(还没做国际化之前)")
89+
task_type: str = Field("chat", description="任务类型:chat / qa / summarization 等")
90+
91+
92+
class PromptPreviewResponse(BaseModel):
93+
rewritten_prompt: str
94+
target_language: str
95+
applied_policies: List[str]
96+
warnings: List[str]
97+
98+
99+
class PolicyEvaluationRequest(BaseModel):
100+
tenant_id: str
101+
region: Optional[str] = None
102+
language: Optional[str] = None
103+
content: str = Field(..., description="最终要发给大模型的内容(或用户问题)")
104+
105+
106+
class PolicyEvaluationResponse(BaseModel):
107+
allowed: bool
108+
reasons: List[str]
109+
matched_rules: List[str]
110+
111+
112+
class EventLogItem(BaseModel):
113+
id: str
114+
tenant_id: str
115+
region: str
116+
language: str
117+
event_type: str # prompt_preview / blocked / allowed ...
118+
created_at: datetime
119+
payload: Dict[str, Any]
120+
121+
122+
class EventIn(BaseModel):
123+
tenant_id: str
124+
region: str
125+
language: str
126+
event_type: str
127+
payload: Dict[str, Any]
128+
129+
130+
# -----------------------------
131+
# 工具函数
132+
# -----------------------------
133+
134+
135+
def get_tenant_cfg(tenant_id: str) -> RegionConfig:
136+
cfg = TENANT_CONFIGS.get(tenant_id)
137+
if not cfg:
138+
raise HTTPException(status_code=404, detail=f"Unknown tenant_id: {tenant_id}")
139+
return cfg
140+
141+
142+
def detect_or_default_language(req_lang: Optional[str], cfg: RegionConfig) -> str:
143+
"""
144+
1)如果请求里有 language 并且在允许列表里,直接用
145+
2)否则 fallback 到租户默认语言
146+
后面可以在这里接入自动语言检测
147+
"""
148+
if req_lang and req_lang in cfg.allowed_languages:
149+
return req_lang
150+
return cfg.default_language
151+
152+
153+
def apply_governance_wrappers(
154+
prompt: str,
155+
target_language: str,
156+
cfg: RegionConfig,
157+
) -> (str, List[str], List[str]):
158+
"""
159+
根据区域 / 语言,加一些统一前后缀、免责声明之类的包装
160+
"""
161+
applied_policies: List[str] = []
162+
warnings: List[str] = []
163+
164+
# 语言约束(要求模型用指定语言回答)
165+
if target_language.startswith("zh"):
166+
language_hint = "请使用简体中文回答用户问题。"
167+
elif target_language.startswith("en"):
168+
language_hint = "Please answer the user in English."
169+
elif target_language.startswith("th"):
170+
language_hint = "กรุณาตอบเป็นภาษาไทย"
171+
else:
172+
language_hint = f"Please answer in language: {target_language}."
173+
174+
applied_policies.append("language_hint")
175+
176+
# 幻觉风险提醒(对终端用户可见)
177+
if cfg.hallucination_notice:
178+
if target_language.startswith("zh"):
179+
warnings.append("本回答由大模型生成,可能存在不准确或过期信息,请结合实际业务进行判断。")
180+
elif target_language.startswith("en"):
181+
warnings.append(
182+
"This answer is generated by a large language model and may contain inaccuracies. "
183+
"Please verify before applying to production."
184+
)
185+
else:
186+
warnings.append(
187+
"This answer is generated by an AI model and may be inaccurate. "
188+
"Please verify before using it in critical scenarios."
189+
)
190+
applied_policies.append("hallucination_notice")
191+
192+
# PII 强化(这里只打标,实际可以在这里接 OPEA guardrails)
193+
if cfg.pii_strict:
194+
applied_policies.append("pii_strict")
195+
196+
rewritten = f"{language_hint}\n\n{prompt}"
197+
return rewritten, applied_policies, warnings
198+
199+
200+
def simple_blocked_keyword_check(content: str, cfg: RegionConfig) -> (bool, List[str], List[str]):
201+
"""
202+
极简关键词命中逻辑,后续可以换成更复杂的策略或接入 OPEA guardrails
203+
"""
204+
matched = []
205+
lower = content.lower()
206+
for kw in cfg.blocked_keywords:
207+
if kw and kw.lower() in lower:
208+
matched.append(kw)
209+
210+
if matched:
211+
reasons = [f"命中敏感词: {', '.join(matched)}"]
212+
return False, reasons, matched
213+
return True, [], []
214+
215+
216+
# -----------------------------
217+
# API 定义
218+
# -----------------------------
219+
220+
221+
@app.get("/healthz")
222+
def healthz():
223+
return {"status": "ok", "service": "globalization-governance", "version": "0.1.0"}
224+
225+
226+
@app.post("/v1/globalization/prompt/preview", response_model=PromptPreviewResponse)
227+
def preview_prompt(req: PromptPreviewRequest):
228+
cfg = get_tenant_cfg(req.tenant_id)
229+
target_lang = detect_or_default_language(req.language, cfg)
230+
231+
rewritten, policies, warnings = apply_governance_wrappers(
232+
prompt=req.raw_prompt,
233+
target_language=target_lang,
234+
cfg=cfg,
235+
)
236+
237+
EVENT_LOG.append(
238+
EventLogItem(
239+
id=str(uuid4()),
240+
tenant_id=req.tenant_id,
241+
region=req.region or cfg.region_code,
242+
language=target_lang,
243+
event_type="prompt_preview",
244+
created_at=datetime.utcnow(),
245+
payload={"task_type": req.task_type},
246+
).model_dump()
247+
)
248+
249+
return PromptPreviewResponse(
250+
rewritten_prompt=rewritten,
251+
target_language=target_lang,
252+
applied_policies=policies,
253+
warnings=warnings,
254+
)
255+
256+
257+
@app.post("/v1/globalization/policy/evaluate", response_model=PolicyEvaluationResponse)
258+
def evaluate_policy(req: PolicyEvaluationRequest):
259+
cfg = get_tenant_cfg(req.tenant_id)
260+
lang = detect_or_default_language(req.language, cfg)
261+
262+
allowed, reasons, matched = simple_blocked_keyword_check(req.content, cfg)
263+
264+
EVENT_LOG.append(
265+
EventLogItem(
266+
id=str(uuid4()),
267+
tenant_id=req.tenant_id,
268+
region=req.region or cfg.region_code,
269+
language=lang,
270+
event_type="blocked" if not allowed else "allowed",
271+
created_at=datetime.utcnow(),
272+
payload={"matched_rules": matched},
273+
).model_dump()
274+
)
275+
276+
return PolicyEvaluationResponse(
277+
allowed=allowed,
278+
reasons=reasons,
279+
matched_rules=matched,
280+
)
281+
282+
283+
@app.post("/v1/globalization/events", response_model=EventLogItem)
284+
def log_event(ev: EventIn):
285+
"""
286+
通用事件记录接口,方便前端 / 其它服务打点
287+
"""
288+
item = EventLogItem(
289+
id=str(uuid4()),
290+
tenant_id=ev.tenant_id,
291+
region=ev.region,
292+
language=ev.language,
293+
event_type=ev.event_type,
294+
created_at=datetime.utcnow(),
295+
payload=ev.payload,
296+
)
297+
EVENT_LOG.append(item.model_dump())
298+
return item
299+
300+
301+
@app.get("/v1/globalization/events", response_model=List[EventLogItem])
302+
def list_events(limit: int = 100):
303+
"""
304+
国际化运营看板的数据源:
305+
- 每条事件包含 tenant / region / language / event_type / payload
306+
"""
307+
return EVENT_LOG[-limit:]
308+
309+
310+
if __name__ == "__main__":
311+
import uvicorn
312+
313+
uvicorn.run("app.main:app", host="0.0.0.0", port=8080, reload=True)

0 commit comments

Comments
 (0)