Upload 19 files
Browse files- core.py +1 -0
- routes_team_chat.py +26 -2
- schemas.py +6 -0
- services_legacy.py +193 -17
core.py
CHANGED
|
@@ -39,6 +39,7 @@ issues_collection = db.project_issues
|
|
| 39 |
image_assets_collection = db.image_assets
|
| 40 |
team_chat_collection = db.team_chat_history
|
| 41 |
team_docs_collection = db.team_documents
|
|
|
|
| 42 |
|
| 43 |
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", os.path.join(os.path.dirname(__file__), "uploads"))
|
| 44 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
|
|
| 39 |
image_assets_collection = db.image_assets
|
| 40 |
team_chat_collection = db.team_chat_history
|
| 41 |
team_docs_collection = db.team_documents
|
| 42 |
+
team_doc_chunks_collection = db.team_document_chunks
|
| 43 |
|
| 44 |
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", os.path.join(os.path.dirname(__file__), "uploads"))
|
| 45 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
routes_team_chat.py
CHANGED
|
@@ -8,7 +8,7 @@ from fastapi import APIRouter, File, Form, Header, HTTPException, UploadFile, We
|
|
| 8 |
|
| 9 |
from core import TEAM_AGENT_MODEL, projects_collection, team_chat_collection, teams_collection
|
| 10 |
from prompts import TEAM_AGENT_SYSTEM_PROMPT
|
| 11 |
-
from schemas import TeamChatRequest
|
| 12 |
from services import (
|
| 13 |
build_document_grounded_answer,
|
| 14 |
build_requirement_node_options_from_documents,
|
|
@@ -322,7 +322,7 @@ async def upload_team_document(
|
|
| 322 |
"mime_type": doc["mime_type"],
|
| 323 |
"created_at": doc["created_at"],
|
| 324 |
"updated_at": doc["updated_at"],
|
| 325 |
-
"total_nodes": (doc.get("tree") or {}).get("total_nodes", 0),
|
| 326 |
}
|
| 327 |
}
|
| 328 |
|
|
@@ -363,6 +363,30 @@ async def upload_team_chat_images(
|
|
| 363 |
}
|
| 364 |
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
@router.post("/teams/chat")
|
| 367 |
async def team_chat(req: TeamChatRequest, x_session_token: Optional[str] = Header(None, alias="X-Session-Token")):
|
| 368 |
user = require_session_user(x_session_token)
|
|
|
|
| 8 |
|
| 9 |
from core import TEAM_AGENT_MODEL, projects_collection, team_chat_collection, teams_collection
|
| 10 |
from prompts import TEAM_AGENT_SYSTEM_PROMPT
|
| 11 |
+
from schemas import TeamChatMessageRequest, TeamChatRequest
|
| 12 |
from services import (
|
| 13 |
build_document_grounded_answer,
|
| 14 |
build_requirement_node_options_from_documents,
|
|
|
|
| 322 |
"mime_type": doc["mime_type"],
|
| 323 |
"created_at": doc["created_at"],
|
| 324 |
"updated_at": doc["updated_at"],
|
| 325 |
+
"total_nodes": (doc.get("tree") or {}).get("total_nodes", (doc.get("tree_meta") or {}).get("total_nodes", 0)),
|
| 326 |
}
|
| 327 |
}
|
| 328 |
|
|
|
|
| 363 |
}
|
| 364 |
|
| 365 |
|
| 366 |
+
@router.post("/teams/{team_id}/chat/messages")
|
| 367 |
+
async def post_team_chat_message(
|
| 368 |
+
team_id: str,
|
| 369 |
+
req: TeamChatMessageRequest,
|
| 370 |
+
x_session_token: Optional[str] = Header(None, alias="X-Session-Token"),
|
| 371 |
+
):
|
| 372 |
+
user = require_session_user(x_session_token)
|
| 373 |
+
_assert_team_project_access(user, team_id, req.project_id)
|
| 374 |
+
|
| 375 |
+
content = (req.message or "").strip()
|
| 376 |
+
if not content and not (req.attachment_urls or []):
|
| 377 |
+
raise HTTPException(status_code=400, detail="Message is empty")
|
| 378 |
+
|
| 379 |
+
user_msg = save_team_chat_message(
|
| 380 |
+
team_id,
|
| 381 |
+
"user",
|
| 382 |
+
content,
|
| 383 |
+
req.project_id,
|
| 384 |
+
attachment_urls=req.attachment_urls,
|
| 385 |
+
)
|
| 386 |
+
await _broadcast_team_chat_message(team_id, req.project_id, user_msg)
|
| 387 |
+
return {"message": user_msg}
|
| 388 |
+
|
| 389 |
+
|
| 390 |
@router.post("/teams/chat")
|
| 391 |
async def team_chat(req: TeamChatRequest, x_session_token: Optional[str] = Header(None, alias="X-Session-Token")):
|
| 392 |
user = require_session_user(x_session_token)
|
schemas.py
CHANGED
|
@@ -128,6 +128,12 @@ class TeamChatRequest(BaseModel):
|
|
| 128 |
require_bot_mention: bool = True
|
| 129 |
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
class TeamChatToolCreateIssue(BaseModel):
|
| 132 |
title: str
|
| 133 |
description: str = ""
|
|
|
|
| 128 |
require_bot_mention: bool = True
|
| 129 |
|
| 130 |
|
| 131 |
+
class TeamChatMessageRequest(BaseModel):
|
| 132 |
+
message: str
|
| 133 |
+
project_id: Optional[str] = None
|
| 134 |
+
attachment_urls: List[str] = Field(default_factory=list)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
class TeamChatToolCreateIssue(BaseModel):
|
| 138 |
title: str
|
| 139 |
description: str = ""
|
services_legacy.py
CHANGED
|
@@ -49,6 +49,7 @@ from core import (
|
|
| 49 |
tasks_collection,
|
| 50 |
TEAM_AGENT_MODEL,
|
| 51 |
team_chat_collection,
|
|
|
|
| 52 |
team_docs_collection,
|
| 53 |
teams_collection,
|
| 54 |
users_collection,
|
|
@@ -66,6 +67,9 @@ _tts_tokenizer = None
|
|
| 66 |
_tts_model = None
|
| 67 |
_last_auto_compact_ts = 0.0
|
| 68 |
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
def get_vn_now() -> datetime:
|
| 71 |
return datetime.now(tz=VN_TZ)
|
|
@@ -638,6 +642,133 @@ def _safe_decode_text(raw_bytes: bytes, fallback_name: str = "") -> str:
|
|
| 638 |
return raw_bytes.decode("utf-8", errors="ignore")
|
| 639 |
|
| 640 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
def build_document_tree(text: str) -> Dict[str, Any]:
|
| 642 |
lines = (text or "").splitlines()
|
| 643 |
nodes: List[Dict[str, Any]] = []
|
|
@@ -813,24 +944,49 @@ def save_team_document(
|
|
| 813 |
raw_bytes: bytes,
|
| 814 |
) -> Dict[str, Any]:
|
| 815 |
text = _safe_decode_text(raw_bytes, file_name)
|
| 816 |
-
tree = build_document_tree(text)
|
| 817 |
contextual_meta = _contextualize_document_tree(file_name=file_name, text=text, tree=tree)
|
|
|
|
|
|
|
|
|
|
| 818 |
doc = {
|
| 819 |
-
"id":
|
| 820 |
"team_id": team_id,
|
| 821 |
"project_id": project_id,
|
| 822 |
"name": file_name,
|
| 823 |
"mime_type": mime_type,
|
| 824 |
"uploader_id": uploader_id,
|
| 825 |
-
"
|
| 826 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
"contextual_global_summary": contextual_meta.get("global_summary", ""),
|
| 828 |
"contextual_meta": contextual_meta,
|
| 829 |
-
"created_at":
|
| 830 |
-
"updated_at":
|
| 831 |
}
|
| 832 |
-
|
| 833 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
|
| 835 |
|
| 836 |
def list_team_documents(team_id: str, project_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
@@ -850,19 +1006,18 @@ def list_team_documents(team_id: str, project_id: Optional[str] = None) -> List[
|
|
| 850 |
"uploader_id": 1,
|
| 851 |
"created_at": 1,
|
| 852 |
"updated_at": 1,
|
| 853 |
-
"tree
|
| 854 |
-
"
|
| 855 |
-
"tree.nodes.parent_id": 1,
|
| 856 |
-
"tree.nodes.level": 1,
|
| 857 |
-
"tree.nodes.title": 1,
|
| 858 |
-
"tree.nodes.summary": 1,
|
| 859 |
-
"tree.nodes.contextual_summary": 1,
|
| 860 |
},
|
| 861 |
).sort("updated_at", -1)
|
| 862 |
)
|
| 863 |
for doc in docs:
|
| 864 |
-
tree =
|
| 865 |
nodes = tree.get("nodes") or []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
doc["node_catalog"] = [
|
| 867 |
{
|
| 868 |
"id": node.get("id"),
|
|
@@ -887,7 +1042,28 @@ def get_team_documents_by_ids(team_id: str, doc_ids: List[str], project_id: Opti
|
|
| 887 |
query: Dict[str, Any] = {"team_id": team_id, "id": {"$in": doc_ids}}
|
| 888 |
if project_id:
|
| 889 |
query["project_id"] = project_id
|
| 890 |
-
docs = list(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
by_id = {doc.get("id"): doc for doc in docs}
|
| 892 |
ordered = [by_id[doc_id] for doc_id in doc_ids if doc_id in by_id]
|
| 893 |
return ordered
|
|
|
|
| 49 |
tasks_collection,
|
| 50 |
TEAM_AGENT_MODEL,
|
| 51 |
team_chat_collection,
|
| 52 |
+
team_doc_chunks_collection,
|
| 53 |
team_docs_collection,
|
| 54 |
teams_collection,
|
| 55 |
users_collection,
|
|
|
|
| 67 |
_tts_model = None
|
| 68 |
_last_auto_compact_ts = 0.0
|
| 69 |
|
| 70 |
+
TEAM_DOC_NODE_CONTENT_LIMIT = int(os.environ.get("TEAM_DOC_NODE_CONTENT_LIMIT", "2400"))
|
| 71 |
+
TEAM_DOC_NODE_CHUNK_SIZE = int(os.environ.get("TEAM_DOC_NODE_CHUNK_SIZE", "80"))
|
| 72 |
+
|
| 73 |
|
| 74 |
def get_vn_now() -> datetime:
|
| 75 |
return datetime.now(tz=VN_TZ)
|
|
|
|
| 642 |
return raw_bytes.decode("utf-8", errors="ignore")
|
| 643 |
|
| 644 |
|
| 645 |
+
def _split_large_node_content(tree: Dict[str, Any], max_chars: int = TEAM_DOC_NODE_CONTENT_LIMIT) -> Dict[str, Any]:
|
| 646 |
+
nodes = tree.get("nodes") or []
|
| 647 |
+
if not nodes:
|
| 648 |
+
return tree
|
| 649 |
+
|
| 650 |
+
by_id: Dict[str, Dict[str, Any]] = {str(node.get("id") or ""): node for node in nodes if node.get("id")}
|
| 651 |
+
root_id = str(tree.get("root_id") or "root")
|
| 652 |
+
next_split_idx = 0
|
| 653 |
+
normalized_nodes: List[Dict[str, Any]] = []
|
| 654 |
+
|
| 655 |
+
for node in nodes:
|
| 656 |
+
node_id = str(node.get("id") or "")
|
| 657 |
+
clone = dict(node)
|
| 658 |
+
clone["children"] = list(node.get("children") or [])
|
| 659 |
+
content = str(clone.get("content") or "")
|
| 660 |
+
|
| 661 |
+
if node_id == root_id or len(content) <= max_chars:
|
| 662 |
+
normalized_nodes.append(clone)
|
| 663 |
+
continue
|
| 664 |
+
|
| 665 |
+
parts = [content[i : i + max_chars].strip() for i in range(0, len(content), max_chars)]
|
| 666 |
+
parts = [part for part in parts if part]
|
| 667 |
+
if not parts:
|
| 668 |
+
clone["content"] = ""
|
| 669 |
+
normalized_nodes.append(clone)
|
| 670 |
+
continue
|
| 671 |
+
|
| 672 |
+
clone["content"] = parts[0]
|
| 673 |
+
clone["summary"] = _clip_text(parts[0], 280)
|
| 674 |
+
clone["contextual_summary"] = _clip_text(parts[0], 260)
|
| 675 |
+
|
| 676 |
+
parent_level = int(clone.get("level") or 1)
|
| 677 |
+
continuation_children = list(clone.get("children") or [])
|
| 678 |
+
title = str(clone.get("title") or "Section")
|
| 679 |
+
normalized_nodes.append(clone)
|
| 680 |
+
|
| 681 |
+
for idx, part in enumerate(parts[1:], start=2):
|
| 682 |
+
next_split_idx += 1
|
| 683 |
+
split_id = f"{node_id}_part_{next_split_idx}"
|
| 684 |
+
while split_id in by_id:
|
| 685 |
+
next_split_idx += 1
|
| 686 |
+
split_id = f"{node_id}_part_{next_split_idx}"
|
| 687 |
+
|
| 688 |
+
child_node = {
|
| 689 |
+
"id": split_id,
|
| 690 |
+
"parent_id": node_id,
|
| 691 |
+
"level": parent_level + 1,
|
| 692 |
+
"title": f"{title} (phần {idx})",
|
| 693 |
+
"summary": _clip_text(part, 280),
|
| 694 |
+
"contextual_summary": _clip_text(part, 260),
|
| 695 |
+
"scope": f"Nội dung tiếp theo của: {title}",
|
| 696 |
+
"content": part,
|
| 697 |
+
"children": [],
|
| 698 |
+
}
|
| 699 |
+
continuation_children.append(split_id)
|
| 700 |
+
by_id[split_id] = child_node
|
| 701 |
+
normalized_nodes.append(child_node)
|
| 702 |
+
|
| 703 |
+
clone["children"] = continuation_children
|
| 704 |
+
|
| 705 |
+
return {
|
| 706 |
+
"root_id": root_id,
|
| 707 |
+
"nodes": normalized_nodes,
|
| 708 |
+
"total_nodes": len(normalized_nodes),
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
def _chunk_document_nodes(nodes: List[Dict[str, Any]], chunk_size: int = TEAM_DOC_NODE_CHUNK_SIZE) -> List[List[Dict[str, Any]]]:
|
| 713 |
+
if not nodes:
|
| 714 |
+
return []
|
| 715 |
+
size = max(1, int(chunk_size))
|
| 716 |
+
return [nodes[i : i + size] for i in range(0, len(nodes), size)]
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
def _store_team_document_chunks(doc_id: str, nodes: List[Dict[str, Any]], created_at: str) -> None:
|
| 720 |
+
chunks = _chunk_document_nodes(nodes)
|
| 721 |
+
if not chunks:
|
| 722 |
+
return
|
| 723 |
+
|
| 724 |
+
chunk_docs = [
|
| 725 |
+
{
|
| 726 |
+
"id": str(uuid.uuid4()),
|
| 727 |
+
"doc_id": doc_id,
|
| 728 |
+
"chunk_index": idx,
|
| 729 |
+
"nodes": chunk_nodes,
|
| 730 |
+
"created_at": created_at,
|
| 731 |
+
"updated_at": created_at,
|
| 732 |
+
}
|
| 733 |
+
for idx, chunk_nodes in enumerate(chunks)
|
| 734 |
+
]
|
| 735 |
+
team_doc_chunks_collection.insert_many(chunk_docs)
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
def _load_team_document_nodes(doc_id: str) -> List[Dict[str, Any]]:
|
| 739 |
+
rows = list(
|
| 740 |
+
team_doc_chunks_collection.find(
|
| 741 |
+
{"doc_id": doc_id},
|
| 742 |
+
{"_id": 0, "nodes": 1},
|
| 743 |
+
).sort("chunk_index", 1)
|
| 744 |
+
)
|
| 745 |
+
nodes: List[Dict[str, Any]] = []
|
| 746 |
+
for row in rows:
|
| 747 |
+
chunk_nodes = row.get("nodes")
|
| 748 |
+
if isinstance(chunk_nodes, list):
|
| 749 |
+
nodes.extend(chunk_nodes)
|
| 750 |
+
return nodes
|
| 751 |
+
|
| 752 |
+
|
| 753 |
+
def _resolve_document_tree(doc: Dict[str, Any]) -> Dict[str, Any]:
|
| 754 |
+
inline_tree = doc.get("tree") if isinstance(doc.get("tree"), dict) else None
|
| 755 |
+
if inline_tree and isinstance(inline_tree.get("nodes"), list):
|
| 756 |
+
total_nodes = inline_tree.get("total_nodes")
|
| 757 |
+
if not isinstance(total_nodes, int):
|
| 758 |
+
inline_tree["total_nodes"] = len(inline_tree.get("nodes") or [])
|
| 759 |
+
return inline_tree
|
| 760 |
+
|
| 761 |
+
tree_meta = doc.get("tree_meta") if isinstance(doc.get("tree_meta"), dict) else {}
|
| 762 |
+
nodes = _load_team_document_nodes(str(doc.get("id") or ""))
|
| 763 |
+
root_id = str(tree_meta.get("root_id") or "root")
|
| 764 |
+
total_nodes = int(tree_meta.get("total_nodes") or len(nodes))
|
| 765 |
+
return {
|
| 766 |
+
"root_id": root_id,
|
| 767 |
+
"nodes": nodes,
|
| 768 |
+
"total_nodes": total_nodes,
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
|
| 772 |
def build_document_tree(text: str) -> Dict[str, Any]:
|
| 773 |
lines = (text or "").splitlines()
|
| 774 |
nodes: List[Dict[str, Any]] = []
|
|
|
|
| 944 |
raw_bytes: bytes,
|
| 945 |
) -> Dict[str, Any]:
|
| 946 |
text = _safe_decode_text(raw_bytes, file_name)
|
| 947 |
+
tree = _split_large_node_content(build_document_tree(text))
|
| 948 |
contextual_meta = _contextualize_document_tree(file_name=file_name, text=text, tree=tree)
|
| 949 |
+
doc_id = str(uuid.uuid4())
|
| 950 |
+
now_iso = get_vn_now().isoformat()
|
| 951 |
+
|
| 952 |
doc = {
|
| 953 |
+
"id": doc_id,
|
| 954 |
"team_id": team_id,
|
| 955 |
"project_id": project_id,
|
| 956 |
"name": file_name,
|
| 957 |
"mime_type": mime_type,
|
| 958 |
"uploader_id": uploader_id,
|
| 959 |
+
"tree_meta": {
|
| 960 |
+
"root_id": tree.get("root_id", "root"),
|
| 961 |
+
"total_nodes": int(tree.get("total_nodes") or len(tree.get("nodes") or [])),
|
| 962 |
+
"storage": "chunked",
|
| 963 |
+
"chunk_size": TEAM_DOC_NODE_CHUNK_SIZE,
|
| 964 |
+
},
|
| 965 |
+
"text_meta": {
|
| 966 |
+
"raw_bytes": len(raw_bytes),
|
| 967 |
+
"decoded_chars": len(text),
|
| 968 |
+
"storage": "transient",
|
| 969 |
+
},
|
| 970 |
"contextual_global_summary": contextual_meta.get("global_summary", ""),
|
| 971 |
"contextual_meta": contextual_meta,
|
| 972 |
+
"created_at": now_iso,
|
| 973 |
+
"updated_at": now_iso,
|
| 974 |
}
|
| 975 |
+
|
| 976 |
+
try:
|
| 977 |
+
team_docs_collection.insert_one(doc)
|
| 978 |
+
_store_team_document_chunks(doc_id=doc_id, nodes=tree.get("nodes") or [], created_at=now_iso)
|
| 979 |
+
except Exception:
|
| 980 |
+
team_docs_collection.delete_many({"id": doc_id})
|
| 981 |
+
team_doc_chunks_collection.delete_many({"doc_id": doc_id})
|
| 982 |
+
raise
|
| 983 |
+
|
| 984 |
+
out_doc = dict(doc)
|
| 985 |
+
out_doc["tree"] = {
|
| 986 |
+
"root_id": tree.get("root_id", "root"),
|
| 987 |
+
"total_nodes": tree.get("total_nodes", len(tree.get("nodes") or [])),
|
| 988 |
+
}
|
| 989 |
+
return out_doc
|
| 990 |
|
| 991 |
|
| 992 |
def list_team_documents(team_id: str, project_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
|
|
| 1006 |
"uploader_id": 1,
|
| 1007 |
"created_at": 1,
|
| 1008 |
"updated_at": 1,
|
| 1009 |
+
"tree": 1,
|
| 1010 |
+
"tree_meta": 1,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1011 |
},
|
| 1012 |
).sort("updated_at", -1)
|
| 1013 |
)
|
| 1014 |
for doc in docs:
|
| 1015 |
+
tree = _resolve_document_tree(doc)
|
| 1016 |
nodes = tree.get("nodes") or []
|
| 1017 |
+
doc["tree"] = {
|
| 1018 |
+
"root_id": tree.get("root_id", "root"),
|
| 1019 |
+
"total_nodes": int(tree.get("total_nodes") or len(nodes)),
|
| 1020 |
+
}
|
| 1021 |
doc["node_catalog"] = [
|
| 1022 |
{
|
| 1023 |
"id": node.get("id"),
|
|
|
|
| 1042 |
query: Dict[str, Any] = {"team_id": team_id, "id": {"$in": doc_ids}}
|
| 1043 |
if project_id:
|
| 1044 |
query["project_id"] = project_id
|
| 1045 |
+
docs = list(
|
| 1046 |
+
team_docs_collection.find(
|
| 1047 |
+
query,
|
| 1048 |
+
{
|
| 1049 |
+
"_id": 0,
|
| 1050 |
+
"id": 1,
|
| 1051 |
+
"team_id": 1,
|
| 1052 |
+
"project_id": 1,
|
| 1053 |
+
"name": 1,
|
| 1054 |
+
"mime_type": 1,
|
| 1055 |
+
"uploader_id": 1,
|
| 1056 |
+
"contextual_global_summary": 1,
|
| 1057 |
+
"contextual_meta": 1,
|
| 1058 |
+
"created_at": 1,
|
| 1059 |
+
"updated_at": 1,
|
| 1060 |
+
"tree": 1,
|
| 1061 |
+
"tree_meta": 1,
|
| 1062 |
+
},
|
| 1063 |
+
)
|
| 1064 |
+
)
|
| 1065 |
+
for doc in docs:
|
| 1066 |
+
doc["tree"] = _resolve_document_tree(doc)
|
| 1067 |
by_id = {doc.get("id"): doc for doc in docs}
|
| 1068 |
ordered = [by_id[doc_id] for doc_id in doc_ids if doc_id in by_id]
|
| 1069 |
return ordered
|