Feature/delta team/epic r04 (#7)
CI / test (push) Canceled after 0s

## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
This commit was merged in pull request #7.
This commit is contained in:
2026-08-31 05:15:13 +00:00
committed by gitea-admin
co-authored by anhtnm1 huongltt35 Nam Pham Dinh Thanh vudt15 Hiep Ha Van lamhv7
parent 86c27e2e79
commit f9f6bc01fd
496 changed files with 68421 additions and 19688 deletions
+10
View File
@@ -28,6 +28,11 @@ _PREFIX = "ms365_"
def _strip(name: str) -> str:
"""Bỏ tiền tố ``ms365_`` khỏi tên tool.
MCP đã gom tool theo tên máy chủ nên để tiền tố nữa thành thừa; bộ thực
thi cũ vẫn dispatch theo tên có tiền tố, nên gỡ lúc ra và gắn lại lúc vào.
"""
return name[len(_PREFIX):] if name.startswith(_PREFIX) else name
@@ -63,6 +68,7 @@ def _dispatch(name: str, args: Dict[str, Any]) -> str:
def build_server():
"""Dựng máy chủ MCP cho nhóm tool MS365 và đăng ký hai handler của giao thức."""
import mcp.types as types
from mcp.server.lowlevel import Server
@@ -70,22 +76,26 @@ def build_server():
@app.list_tools()
async def list_tools() -> List["types.Tool"]:
"""Trả về danh sách tool MS365 hiện có, đọc từ cấu hình mới nhất."""
return [types.Tool(**t) for t in _tool_list()]
@app.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> List["types.TextContent"]:
"""Chạy một tool MS365 và trả kết quả về dưới dạng văn bản."""
return [types.TextContent(type="text", text=_dispatch(name, arguments or {}))]
return app
def main() -> None:
"""Điểm vào khi chạy như tiến trình con: phục vụ MCP qua stdio."""
import anyio
from mcp.server.stdio import stdio_server
app = build_server()
async def _run() -> None:
"""Vòng lặp phục vụ, đọc/ghi trên stdio cho tới khi tiến trình cha đóng."""
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
+33 -2
View File
@@ -17,6 +17,11 @@ class ContractModel(BaseModel):
class IdentityContext(ContractModel):
"""Danh tính người gọi kèm phạm vi đã được cấp.
``granted_scopes`` là ``frozenset`` để bản ghi danh tính không bị sửa
trên đường đi giữa các tầng.
"""
actor_id: str = Field(min_length=1, max_length=256)
org_unit: str = Field(min_length=1, max_length=128)
customer: str = Field(min_length=1, max_length=128)
@@ -25,6 +30,10 @@ class IdentityContext(ContractModel):
class SourceCitation(ContractModel):
"""Trích dẫn nguồn cho một mẩu thông tin: hệ thống nào, URL nào, bản nào, lấy lúc nào.
Bắt buộc có để Agent trả lời kèm nguồn kiểm chứng được, không bịa.
"""
system: str = Field(min_length=1, max_length=64)
url: AnyUrl
revision: str = Field(min_length=1, max_length=256)
@@ -33,20 +42,31 @@ class SourceCitation(ContractModel):
@dataclass(frozen=True)
class DispatchResult:
"""Kết quả một lượt gọi tool: thành công hay không, kèm payload trả cho Agent."""
ok: bool
payload: dict[str, Any]
class PolicyDecisionPoint(Protocol):
def decide(self, identity: IdentityContext, tool_name: str, project_id: str) -> bool: ...
"""Nơi quyết định một danh tính có được gọi một tool trên một project hay không."""
def decide(self, identity: IdentityContext, tool_name: str, project_id: str) -> bool:
"""``True`` nếu cho phép lượt gọi này."""
...
class CredentialResolver(Protocol):
def resolve(self, identity: IdentityContext, tool_name: str) -> Any: ...
"""Nơi cấp thông tin xác thực cho tool, tách khỏi chỗ dùng nó."""
def resolve(self, identity: IdentityContext, tool_name: str) -> Any:
"""Trả về thông tin xác thực cho danh tính và tool tương ứng."""
...
@dataclass(frozen=True)
class ProjectContextRuntime:
"""Bộ ba mà mọi lượt gọi tool cần: danh tính, cổng chính sách, nơi cấp thông tin xác thực.
Bất biến (``frozen``) — một lượt gọi không được đổi bối cảnh của lượt khác.
"""
identity: IdentityContext
policy: PolicyDecisionPoint
credential_resolver: CredentialResolver
@@ -56,6 +76,9 @@ class ProviderError(RuntimeError):
"""A provider failure with a caller-safe message and retry classification."""
def __init__(self, code: str, message: str, *, retryable: bool) -> None:
"""``safe_message`` là phần được phép hiện cho người dùng; ``retryable`` cho bên
gọi biết thử lại có ích không, thay vì bắt họ đoán từ chuỗi lỗi.
"""
super().__init__(message)
self.code = code
self.safe_message = message
@@ -67,6 +90,7 @@ ToolHandler = Callable[[ContractModel, Any], dict[str, Any]]
@dataclass(frozen=True)
class ToolTemplate:
"""Khai báo một tool MCP: tên, mô tả, kiểu vào/ra, và hàm xử lý."""
name: str
description: str
input_model: type[ContractModel]
@@ -74,6 +98,7 @@ class ToolTemplate:
handler: ToolHandler
def declaration(self) -> dict[str, Any]:
"""Bản khai báo theo đúng định dạng MCP, schema sinh thẳng từ model Pydantic."""
return {
"name": self.name,
"description": self.description,
@@ -91,6 +116,12 @@ def error_result(
suggested_action: str,
correlation_id: str,
) -> DispatchResult:
"""Dựng một ``DispatchResult`` lỗi theo đúng khuôn chung.
Mọi lỗi trả cho Agent đều phải có ``correlation_id`` để dò lại trong
nhật ký, và ``suggested_action`` để Agent biết nên làm gì tiếp thay vì
chỉ dừng.
"""
return DispatchResult(
ok=False,
payload={
@@ -8,11 +8,24 @@ from ..foundation import IdentityContext, ProviderError
class ChangeProvider(Protocol):
def get_change_context(self, **arguments: Any) -> dict[str, Any]: ...
"""Nguồn cấp bối cảnh thay đổi (commit, PR) cho một project.
Chỉ là hợp đồng: bản cài đặt thật được nối vào qua ``build_provider``.
"""
def get_change_context(self, **arguments: Any) -> dict[str, Any]:
"""Trả về bối cảnh thay đổi (commit, PR) theo tham số của tool."""
...
class UnconfiguredChangeProvider:
"""Bản thay thế khi chưa cấu hình nguồn thay đổi (commit, PR) thật.
Luôn ném lỗi ``UNAVAILABLE`` thay vì trả dữ liệu rỗng — rỗng sẽ bị Agent
hiểu nhầm là "tra rồi, không có gì", còn lỗi thì nói đúng sự thật là
chưa có nguồn nào được nối.
"""
def get_change_context(self, **arguments: Any) -> dict[str, Any]:
"""Luôn báo chưa cấu hình; không cho thử lại."""
raise ProviderError(
"UNAVAILABLE",
"The change provider is not configured for this environment.",
+14 -1
View File
@@ -8,11 +8,24 @@ from ..foundation import IdentityContext, ProviderError
class IssueProvider(Protocol):
def get_issue_context(self, **arguments: Any) -> dict[str, Any]: ...
"""Nguồn cấp bối cảnh issue/ticket cho một project.
Chỉ là hợp đồng: bản cài đặt thật được nối vào qua ``build_provider``.
"""
def get_issue_context(self, **arguments: Any) -> dict[str, Any]:
"""Trả về bối cảnh issue/ticket theo tham số của tool."""
...
class UnconfiguredIssueProvider:
"""Bản thay thế khi chưa cấu hình nguồn issue/ticket thật.
Luôn ném lỗi ``UNAVAILABLE`` thay vì trả dữ liệu rỗng — rỗng sẽ bị Agent
hiểu nhầm là "tra rồi, không có gì", còn lỗi thì nói đúng sự thật là
chưa có nguồn nào được nối.
"""
def get_issue_context(self, **arguments: Any) -> dict[str, Any]:
"""Luôn báo chưa cấu hình; không cho thử lại."""
raise ProviderError(
"UNAVAILABLE",
"The issue provider is not configured for this environment.",
@@ -8,11 +8,24 @@ from ..foundation import IdentityContext, ProviderError
class KnowledgeProvider(Protocol):
def search_knowledge(self, **arguments: Any) -> dict[str, Any]: ...
"""Nguồn cấp bối cảnh tri thức nội bộ cho một project.
Chỉ là hợp đồng: bản cài đặt thật được nối vào qua ``build_provider``.
"""
def search_knowledge(self, **arguments: Any) -> dict[str, Any]:
"""Trả về bối cảnh tri thức nội bộ theo tham số của tool."""
...
class UnconfiguredKnowledgeProvider:
"""Bản thay thế khi chưa cấu hình nguồn tri thức nội bộ thật.
Luôn ném lỗi ``UNAVAILABLE`` thay vì trả dữ liệu rỗng — rỗng sẽ bị Agent
hiểu nhầm là "tra rồi, không có gì", còn lỗi thì nói đúng sự thật là
chưa có nguồn nào được nối.
"""
def search_knowledge(self, **arguments: Any) -> dict[str, Any]:
"""Luôn báo chưa cấu hình; không cho thử lại."""
raise ProviderError(
"UNAVAILABLE",
"The knowledge provider is not configured for this environment.",
+1
View File
@@ -20,4 +20,5 @@ TOOL_NAMES = tuple(tool.name for tool in TOOLS)
def tool_declarations() -> list[dict[str, Any]]:
"""Danh sách khai báo của mọi tool đã đăng ký, để trả về cho ``list_tools``."""
return [tool.declaration() for tool in TOOLS]
+12
View File
@@ -31,6 +31,9 @@ class ProjectScopePolicy:
"""Pilot policy: read scope and exact identity-bound project are both mandatory."""
def decide(self, identity: IdentityContext, tool_name: str, project_id: str) -> bool:
"""Chỉ cho phép khi danh tính có phạm vi ``read`` VÀ project khớp đúng
project gắn với danh tính đó — không cho đọc chéo project.
"""
return "read" in identity.granted_scopes and project_id == identity.project
@@ -43,7 +46,11 @@ PROVIDER_FACTORIES: dict[str, Callable[[IdentityContext], Any]] = {
@dataclass(frozen=True)
class ProjectProviderResolver:
"""Tra provider thật cho từng tool theo bảng ``PROVIDER_FACTORIES``."""
def resolve(self, identity: IdentityContext, tool_name: str) -> Any:
"""Dựng provider cho một tool; tool chưa đăng ký thì báo ``NOT_FOUND`` và
không cho thử lại.
"""
factory = PROVIDER_FACTORIES.get(tool_name)
if factory is None:
raise ProviderError("NOT_FOUND", "The requested tool is not registered.", retryable=False)
@@ -51,6 +58,11 @@ class ProjectProviderResolver:
def _required_environment(name: str) -> str:
"""Đọc một biến môi trường bắt buộc; thiếu thì dừng ngay lúc khởi động.
Thà không chạy còn hơn chạy với cấu hình khuyết rồi lỗi giữa chừng ở một
lượt gọi tool nào đó.
"""
value = os.environ.get(name, "").strip()
if not value:
raise RuntimeError(f"Project Context MCP cannot start: required setting {name} is missing")
+13
View File
@@ -99,6 +99,11 @@ def dispatch(
def build_server(runtime: Optional[ProjectContextRuntime] = None):
"""Dựng máy chủ MCP Project Context.
``runtime`` để trống thì lấy bộ mặc định đọc từ biến môi trường; test
truyền vào bộ giả để không cần cấu hình thật.
"""
from mcp import types
from mcp.server.lowlevel import Server
@@ -108,10 +113,16 @@ def build_server(runtime: Optional[ProjectContextRuntime] = None):
@app.list_tools()
async def list_tools() -> list[types.Tool]:
"""Trả về khai báo của mọi tool đã đăng ký."""
return [types.Tool(**declaration) for declaration in tool_declarations()]
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:
"""Chạy một tool và trả kết quả.
Luôn kèm payload dạng văn bản JSON; chỉ khi thành công mới đính thêm
``structuredContent``, còn lỗi thì bật ``isError``.
"""
result = dispatch(name, arguments or {}, app_runtime)
return types.CallToolResult(
content=[types.TextContent(
@@ -126,12 +137,14 @@ def build_server(runtime: Optional[ProjectContextRuntime] = None):
def main() -> None:
"""Điểm vào khi chạy như tiến trình con: phục vụ MCP qua stdio."""
import anyio
from mcp.server.stdio import stdio_server
app = build_server()
async def _run() -> None:
"""Vòng lặp phục vụ, đọc/ghi trên stdio cho tới khi tiến trình cha đóng."""
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
@@ -12,6 +12,10 @@ from ..foundation import ContractModel, SourceCitation, ToolTemplate
class ChangeContextInput(ContractModel):
"""Tham số vào của ``get_project_change_context``: project và mã thay đổi.
``detail`` chọn độ dài kết quả, ``cursor`` để lấy trang tiếp khi bị cắt bớt.
"""
project_id: str = Field(min_length=1, max_length=128)
change_id: str = Field(min_length=1, max_length=128)
detail: Literal["summary", "standard", "full"] = "standard"
@@ -19,6 +23,11 @@ class ChangeContextInput(ContractModel):
class ChangeContextOutput(ContractModel):
"""Kết quả trả về: mô tả một commit/PR/MR kèm file, commit và issue liên quan.
``source`` là trích dẫn nguồn bắt buộc; ``truncated``/``next_cursor`` cho
Agent biết còn dữ liệu chưa lấy hết.
"""
correlation_id: str
project_id: str
change_id: str
@@ -38,6 +47,7 @@ class ChangeContextOutput(ContractModel):
def _handle(arguments: ContractModel, provider: Any) -> dict[str, Any]:
"""Kiểm tham số theo schema rồi chuyển sang provider đã được cấp cho lượt gọi này."""
request = ChangeContextInput.model_validate(arguments)
return provider.get_change_context(**request.model_dump())
@@ -12,6 +12,10 @@ from ..foundation import ContractModel, SourceCitation, ToolTemplate
class IssueContextInput(ContractModel):
"""Tham số vào của ``get_project_issue_context``: project và mã issue.
``detail`` chọn độ dài kết quả, ``cursor`` để lấy trang tiếp khi bị cắt bớt.
"""
project_id: str = Field(min_length=1, max_length=128)
issue_key: str = Field(min_length=1, max_length=128)
detail: Literal["summary", "standard", "full"] = "standard"
@@ -19,6 +23,7 @@ class IssueContextInput(ContractModel):
class RelatedItem(ContractModel):
"""Một mục liên quan tới issue (issue khác, PR, tài liệu) kèm loại quan hệ."""
item_id: str
relation: str
title: str
@@ -26,6 +31,11 @@ class RelatedItem(ContractModel):
class IssueContextOutput(ContractModel):
"""Kết quả trả về: tiêu đề, trạng thái, mô tả, tiêu chí nghiệm thu và các mục liên quan.
``source`` là trích dẫn nguồn bắt buộc; ``truncated``/``next_cursor`` cho
Agent biết còn dữ liệu chưa lấy hết.
"""
correlation_id: str
project_id: str
issue_key: str
@@ -42,6 +52,7 @@ class IssueContextOutput(ContractModel):
def _handle(arguments: ContractModel, provider: Any) -> dict[str, Any]:
"""Kiểm tham số theo schema rồi chuyển sang provider đã được cấp cho lượt gọi này."""
request = IssueContextInput.model_validate(arguments)
return provider.get_issue_context(**request.model_dump())
@@ -12,6 +12,10 @@ from ..foundation import ContractModel, SourceCitation, ToolTemplate
class KnowledgeSearchInput(ContractModel):
"""Tham số vào của ``search_project_knowledge``: project và câu truy vấn.
``top_k`` chặn trong 1..20 để một lượt tra không nuốt hết cửa sổ ngữ cảnh.
"""
project_id: str = Field(min_length=1, max_length=128)
query: str = Field(min_length=2, max_length=1000)
detail: Literal["summary", "standard", "full"] = "standard"
@@ -21,6 +25,7 @@ class KnowledgeSearchInput(ContractModel):
class KnowledgeItem(ContractModel):
"""Một đoạn tài liệu khớp truy vấn: trích đoạn, điểm số 0..1 và nguồn."""
document_id: str
chunk_id: str
title: str
@@ -30,6 +35,7 @@ class KnowledgeItem(ContractModel):
class KnowledgeSearchOutput(ContractModel):
"""Kết quả trả về: danh sách đoạn khớp đã xếp hạng, mỗi đoạn kèm trích dẫn nguồn."""
correlation_id: str
project_id: str
query: str
@@ -41,6 +47,7 @@ class KnowledgeSearchOutput(ContractModel):
def _handle(arguments: ContractModel, provider: Any) -> dict[str, Any]:
"""Kiểm tham số theo schema rồi chuyển sang provider đã được cấp cho lượt gọi này."""
request = KnowledgeSearchInput.model_validate(arguments)
return provider.search_knowledge(**request.model_dump())