fix: resolve workflow linking on resume, update proposals auto-commit, and validation baseline rules
Browse files- backend/app/agents/validation.py +26 -22
- backend/app/api/workflows.py +1 -1
- backend/app/services/proposal_review_service.py +1 -1
- backend/app/services/reconciliation_service.py +27 -1
- backend/app/workflow/graph.py +3 -3
- backend/app/workflow/nodes/complete.py +32 -10
- backend/app/workflow/nodes/validate.py +18 -5
- backend/tests/test_mcp_e2e.py +1 -0
- docs/{architecture-diagram.md β DocWeave_architecture_diagram.md} +0 -0
- docs/one-pager.md +0 -40
backend/app/agents/validation.py
CHANGED
|
@@ -114,9 +114,16 @@ class RuleValidationAgent:
|
|
| 114 |
failures = []
|
| 115 |
|
| 116 |
for proposal in proposals:
|
| 117 |
-
changes = proposal
|
| 118 |
proposed = changes.get("proposed", changes)
|
| 119 |
-
evidence = proposed.get("evidence")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
if not evidence:
|
| 122 |
failures.append(
|
|
@@ -132,8 +139,8 @@ class RuleValidationAgent:
|
|
| 132 |
),
|
| 133 |
"operator": "required_evidence",
|
| 134 |
"message": (
|
| 135 |
-
"Proposal does not contain evidence
|
| 136 |
-
"
|
| 137 |
),
|
| 138 |
"proposal_ids": [str(proposal.id)],
|
| 139 |
}
|
|
@@ -389,22 +396,27 @@ class RuleValidationAgent:
|
|
| 389 |
|
| 390 |
workspace_id = proposals[0].workspace_id
|
| 391 |
|
| 392 |
-
# Check if workspace has existing knowledge
|
| 393 |
try:
|
| 394 |
from app.database.database import SessionLocal
|
| 395 |
-
from app.models.knowledge_item import KnowledgeItem
|
| 396 |
from app.services.embedding_service import EmbeddingService
|
| 397 |
|
| 398 |
db = SessionLocal()
|
| 399 |
try:
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
.
|
| 403 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
)
|
| 405 |
|
| 406 |
-
# If workspace
|
| 407 |
-
if
|
| 408 |
return [
|
| 409 |
{
|
| 410 |
"status": "PASS",
|
|
@@ -418,8 +430,8 @@ class RuleValidationAgent:
|
|
| 418 |
),
|
| 419 |
"operator": "topic_relevance",
|
| 420 |
"message": (
|
| 421 |
-
"No
|
| 422 |
-
"First documents
|
| 423 |
),
|
| 424 |
"proposal_ids": [
|
| 425 |
str(p.id) for p in proposals
|
|
@@ -429,14 +441,6 @@ class RuleValidationAgent:
|
|
| 429 |
|
| 430 |
# Use embedding service for semantic similarity
|
| 431 |
embedding_service = EmbeddingService()
|
| 432 |
-
|
| 433 |
-
# Get existing knowledge titles+values for comparison
|
| 434 |
-
existing_items = (
|
| 435 |
-
db.query(KnowledgeItem.title, KnowledgeItem.value)
|
| 436 |
-
.filter(KnowledgeItem.workspace_id == workspace_id)
|
| 437 |
-
.limit(50)
|
| 438 |
-
.all()
|
| 439 |
-
)
|
| 440 |
existing_text = " ".join(
|
| 441 |
f"{item.title} {item.value}" for item in existing_items
|
| 442 |
)
|
|
|
|
| 114 |
failures = []
|
| 115 |
|
| 116 |
for proposal in proposals:
|
| 117 |
+
changes = getattr(proposal, "proposed_changes", None) or {}
|
| 118 |
proposed = changes.get("proposed", changes)
|
| 119 |
+
evidence = proposed.get("evidence") or changes.get("evidence")
|
| 120 |
+
|
| 121 |
+
ki = getattr(proposal, "knowledge_item", None)
|
| 122 |
+
if not evidence and ki and getattr(ki, "evidence", None):
|
| 123 |
+
evidence = [
|
| 124 |
+
{"quote": getattr(ev, "quote", ""), "page_number": getattr(ev, "page_number", None)}
|
| 125 |
+
for ev in ki.evidence
|
| 126 |
+
]
|
| 127 |
|
| 128 |
if not evidence:
|
| 129 |
failures.append(
|
|
|
|
| 139 |
),
|
| 140 |
"operator": "required_evidence",
|
| 141 |
"message": (
|
| 142 |
+
"Proposal does not contain required evidence quote "
|
| 143 |
+
"or citation metadata."
|
| 144 |
),
|
| 145 |
"proposal_ids": [str(proposal.id)],
|
| 146 |
}
|
|
|
|
| 396 |
|
| 397 |
workspace_id = proposals[0].workspace_id
|
| 398 |
|
| 399 |
+
# Check if workspace has existing active baseline knowledge
|
| 400 |
try:
|
| 401 |
from app.database.database import SessionLocal
|
| 402 |
+
from app.models.knowledge_item import KnowledgeItem, KnowledgeStatus
|
| 403 |
from app.services.embedding_service import EmbeddingService
|
| 404 |
|
| 405 |
db = SessionLocal()
|
| 406 |
try:
|
| 407 |
+
# Query only ACTIVE (previously committed) items, excluding uncommitted PENDING items
|
| 408 |
+
existing_items = (
|
| 409 |
+
db.query(KnowledgeItem.title, KnowledgeItem.value)
|
| 410 |
+
.filter(
|
| 411 |
+
KnowledgeItem.workspace_id == workspace_id,
|
| 412 |
+
KnowledgeItem.status == KnowledgeStatus.ACTIVE,
|
| 413 |
+
)
|
| 414 |
+
.limit(50)
|
| 415 |
+
.all()
|
| 416 |
)
|
| 417 |
|
| 418 |
+
# If workspace has no active baseline knowledge yet, pass (first doc establishes baseline)
|
| 419 |
+
if not existing_items:
|
| 420 |
return [
|
| 421 |
{
|
| 422 |
"status": "PASS",
|
|
|
|
| 430 |
),
|
| 431 |
"operator": "topic_relevance",
|
| 432 |
"message": (
|
| 433 |
+
"No prior active knowledge to compare against. "
|
| 434 |
+
"First documents establish the workspace baseline."
|
| 435 |
),
|
| 436 |
"proposal_ids": [
|
| 437 |
str(p.id) for p in proposals
|
|
|
|
| 441 |
|
| 442 |
# Use embedding service for semantic similarity
|
| 443 |
embedding_service = EmbeddingService()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
existing_text = " ".join(
|
| 445 |
f"{item.title} {item.value}" for item in existing_items
|
| 446 |
)
|
backend/app/api/workflows.py
CHANGED
|
@@ -92,7 +92,7 @@ def _sync_workflow_review_state(workflow: WorkflowRun, db: Session) -> None:
|
|
| 92 |
finally:
|
| 93 |
executor.close()
|
| 94 |
except Exception:
|
| 95 |
-
workflow.status = WorkflowStatus.
|
| 96 |
if not workflow.completed_at:
|
| 97 |
workflow.completed_at = datetime.now(timezone.utc)
|
| 98 |
db.commit()
|
|
|
|
| 92 |
finally:
|
| 93 |
executor.close()
|
| 94 |
except Exception:
|
| 95 |
+
workflow.status = WorkflowStatus.FAILED
|
| 96 |
if not workflow.completed_at:
|
| 97 |
workflow.completed_at = datetime.now(timezone.utc)
|
| 98 |
db.commit()
|
backend/app/services/proposal_review_service.py
CHANGED
|
@@ -262,7 +262,7 @@ class ProposalReviewService:
|
|
| 262 |
executor.resume(workflow)
|
| 263 |
except Exception:
|
| 264 |
from datetime import datetime, timezone
|
| 265 |
-
workflow.status = WorkflowStatus.
|
| 266 |
if not workflow.completed_at:
|
| 267 |
workflow.completed_at = datetime.now(timezone.utc)
|
| 268 |
self.db.commit()
|
|
|
|
| 262 |
executor.resume(workflow)
|
| 263 |
except Exception:
|
| 264 |
from datetime import datetime, timezone
|
| 265 |
+
workflow.status = WorkflowStatus.FAILED
|
| 266 |
if not workflow.completed_at:
|
| 267 |
workflow.completed_at = datetime.now(timezone.utc)
|
| 268 |
self.db.commit()
|
backend/app/services/reconciliation_service.py
CHANGED
|
@@ -154,6 +154,17 @@ class ReconciliationService:
|
|
| 154 |
):
|
| 155 |
return
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
proposal = Proposal(
|
| 158 |
workspace_id=workspace_id,
|
| 159 |
knowledge_item_id=item.id,
|
|
@@ -171,6 +182,7 @@ class ReconciliationService:
|
|
| 171 |
"summary": item.summary,
|
| 172 |
"attributes": item.attributes,
|
| 173 |
"confidence": item.confidence,
|
|
|
|
| 174 |
},
|
| 175 |
)
|
| 176 |
|
|
@@ -189,6 +201,17 @@ class ReconciliationService:
|
|
| 189 |
):
|
| 190 |
return
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
proposal = Proposal(
|
| 193 |
workspace_id=workspace_id,
|
| 194 |
knowledge_item_id=existing_item.id,
|
|
@@ -211,12 +234,15 @@ class ReconciliationService:
|
|
| 211 |
"summary": new_item.summary,
|
| 212 |
"attributes": new_item.attributes,
|
| 213 |
"confidence": new_item.confidence,
|
|
|
|
| 214 |
},
|
|
|
|
| 215 |
"source_knowledge_item_id": str(new_item.id),
|
| 216 |
},
|
| 217 |
)
|
| 218 |
|
| 219 |
self.db.add(proposal)
|
|
|
|
| 220 |
@staticmethod
|
| 221 |
def _normalize(value: str | None) -> str:
|
| 222 |
if not value:
|
|
@@ -224,4 +250,4 @@ class ReconciliationService:
|
|
| 224 |
|
| 225 |
words = value.lower().strip().split()
|
| 226 |
|
| 227 |
-
return " ".join(
|
|
|
|
| 154 |
):
|
| 155 |
return
|
| 156 |
|
| 157 |
+
evidence_list = [
|
| 158 |
+
{
|
| 159 |
+
"quote": ev.quote,
|
| 160 |
+
"page_number": ev.page_number,
|
| 161 |
+
"section": ev.section,
|
| 162 |
+
"confidence": ev.confidence,
|
| 163 |
+
"source_type": ev.source_type,
|
| 164 |
+
}
|
| 165 |
+
for ev in (item.evidence or [])
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
proposal = Proposal(
|
| 169 |
workspace_id=workspace_id,
|
| 170 |
knowledge_item_id=item.id,
|
|
|
|
| 182 |
"summary": item.summary,
|
| 183 |
"attributes": item.attributes,
|
| 184 |
"confidence": item.confidence,
|
| 185 |
+
"evidence": evidence_list,
|
| 186 |
},
|
| 187 |
)
|
| 188 |
|
|
|
|
| 201 |
):
|
| 202 |
return
|
| 203 |
|
| 204 |
+
evidence_list = [
|
| 205 |
+
{
|
| 206 |
+
"quote": ev.quote,
|
| 207 |
+
"page_number": ev.page_number,
|
| 208 |
+
"section": ev.section,
|
| 209 |
+
"confidence": ev.confidence,
|
| 210 |
+
"source_type": ev.source_type,
|
| 211 |
+
}
|
| 212 |
+
for ev in (new_item.evidence or [])
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
proposal = Proposal(
|
| 216 |
workspace_id=workspace_id,
|
| 217 |
knowledge_item_id=existing_item.id,
|
|
|
|
| 234 |
"summary": new_item.summary,
|
| 235 |
"attributes": new_item.attributes,
|
| 236 |
"confidence": new_item.confidence,
|
| 237 |
+
"evidence": evidence_list,
|
| 238 |
},
|
| 239 |
+
"evidence": evidence_list,
|
| 240 |
"source_knowledge_item_id": str(new_item.id),
|
| 241 |
},
|
| 242 |
)
|
| 243 |
|
| 244 |
self.db.add(proposal)
|
| 245 |
+
|
| 246 |
@staticmethod
|
| 247 |
def _normalize(value: str | None) -> str:
|
| 248 |
if not value:
|
|
|
|
| 250 |
|
| 251 |
words = value.lower().strip().split()
|
| 252 |
|
| 253 |
+
return " ".join(words)
|
backend/app/workflow/graph.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
from langgraph.graph import END, START, StateGraph
|
|
@@ -226,10 +226,10 @@ def build_workflow(
|
|
| 226 |
"complete",
|
| 227 |
)
|
| 228 |
|
| 229 |
-
# After human approval/resume,
|
| 230 |
graph.add_edge(
|
| 231 |
"human_review",
|
| 232 |
-
"
|
| 233 |
)
|
| 234 |
|
| 235 |
graph.add_edge(
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
from langgraph.graph import END, START, StateGraph
|
|
|
|
| 226 |
"complete",
|
| 227 |
)
|
| 228 |
|
| 229 |
+
# After human approval/resume, route through link before completion.
|
| 230 |
graph.add_edge(
|
| 231 |
"human_review",
|
| 232 |
+
"link",
|
| 233 |
)
|
| 234 |
|
| 235 |
graph.add_edge(
|
backend/app/workflow/nodes/complete.py
CHANGED
|
@@ -19,10 +19,11 @@ def complete(
|
|
| 19 |
"""
|
| 20 |
from sqlalchemy.orm import Session
|
| 21 |
from app.database.database import SessionLocal
|
| 22 |
-
from app.models.proposal import Proposal, ProposalStatus
|
| 23 |
from app.models.knowledge_item import KnowledgeItem, KnowledgeStatus
|
| 24 |
from app.models.commit import Commit
|
| 25 |
from datetime import datetime, timezone
|
|
|
|
| 26 |
import logging
|
| 27 |
|
| 28 |
_log = logging.getLogger(__name__)
|
|
@@ -80,14 +81,11 @@ def complete(
|
|
| 80 |
tracker.end_stage("complete")
|
| 81 |
return state
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
Proposal.status == ProposalStatus.PENDING,
|
| 89 |
-
)
|
| 90 |
-
.all()
|
| 91 |
)
|
| 92 |
|
| 93 |
_log.info(
|
|
@@ -102,6 +100,30 @@ def complete(
|
|
| 102 |
proposal.status = ProposalStatus.APPROVED
|
| 103 |
proposal.reviewed_at = now
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
commit = Commit(
|
| 106 |
workspace_id=proposal.workspace_id,
|
| 107 |
proposal_id=proposal.id,
|
|
@@ -111,7 +133,7 @@ def complete(
|
|
| 111 |
)
|
| 112 |
db.add(commit)
|
| 113 |
|
| 114 |
-
# Promote knowledge items to ACTIVE
|
| 115 |
for ki in knowledge_items:
|
| 116 |
if ki.status == KnowledgeStatus.PENDING:
|
| 117 |
ki.status = KnowledgeStatus.ACTIVE
|
|
|
|
| 19 |
"""
|
| 20 |
from sqlalchemy.orm import Session
|
| 21 |
from app.database.database import SessionLocal
|
| 22 |
+
from app.models.proposal import Proposal, ProposalStatus, ProposalType
|
| 23 |
from app.models.knowledge_item import KnowledgeItem, KnowledgeStatus
|
| 24 |
from app.models.commit import Commit
|
| 25 |
from datetime import datetime, timezone
|
| 26 |
+
import uuid
|
| 27 |
import logging
|
| 28 |
|
| 29 |
_log = logging.getLogger(__name__)
|
|
|
|
| 81 |
tracker.end_stage("complete")
|
| 82 |
return state
|
| 83 |
|
| 84 |
+
from app.services.proposal_review_service import ProposalReviewService
|
| 85 |
+
review_service = ProposalReviewService(db)
|
| 86 |
+
proposals = review_service.get_pending_for_document_version(
|
| 87 |
+
workspace_id=state.workspace_id,
|
| 88 |
+
document_version_id=state.document_version_id,
|
|
|
|
|
|
|
|
|
|
| 89 |
)
|
| 90 |
|
| 91 |
_log.info(
|
|
|
|
| 100 |
proposal.status = ProposalStatus.APPROVED
|
| 101 |
proposal.reviewed_at = now
|
| 102 |
|
| 103 |
+
if proposal.proposal_type == ProposalType.UPDATE and proposal.knowledge_item_id:
|
| 104 |
+
target_item = db.query(KnowledgeItem).filter(KnowledgeItem.id == proposal.knowledge_item_id).first()
|
| 105 |
+
if target_item:
|
| 106 |
+
changes = proposal.proposed_changes or {}
|
| 107 |
+
proposed = changes.get("proposed", {})
|
| 108 |
+
if "value" in proposed:
|
| 109 |
+
target_item.value = proposed["value"]
|
| 110 |
+
if "summary" in proposed:
|
| 111 |
+
target_item.summary = proposed["summary"]
|
| 112 |
+
if "attributes" in proposed:
|
| 113 |
+
target_item.attributes = proposed["attributes"]
|
| 114 |
+
if "confidence" in proposed:
|
| 115 |
+
target_item.confidence = proposed["confidence"]
|
| 116 |
+
target_item.status = KnowledgeStatus.ACTIVE
|
| 117 |
+
|
| 118 |
+
source_id = changes.get("source_knowledge_item_id")
|
| 119 |
+
if source_id:
|
| 120 |
+
try:
|
| 121 |
+
src_item = db.query(KnowledgeItem).filter(KnowledgeItem.id == uuid.UUID(str(source_id))).first()
|
| 122 |
+
if src_item:
|
| 123 |
+
src_item.status = KnowledgeStatus.SUPERSEDED
|
| 124 |
+
except Exception:
|
| 125 |
+
pass
|
| 126 |
+
|
| 127 |
commit = Commit(
|
| 128 |
workspace_id=proposal.workspace_id,
|
| 129 |
proposal_id=proposal.id,
|
|
|
|
| 133 |
)
|
| 134 |
db.add(commit)
|
| 135 |
|
| 136 |
+
# Promote remaining PENDING knowledge items for this doc version to ACTIVE
|
| 137 |
for ki in knowledge_items:
|
| 138 |
if ki.status == KnowledgeStatus.PENDING:
|
| 139 |
ki.status = KnowledgeStatus.ACTIVE
|
backend/app/workflow/nodes/validate.py
CHANGED
|
@@ -35,13 +35,26 @@ def validate(
|
|
| 35 |
new_item_ids = {str(item.id) for item in new_items}
|
| 36 |
|
| 37 |
if not new_item_ids:
|
| 38 |
-
state.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
state.metadata["validation_summary"] = {
|
| 40 |
-
"status": "
|
| 41 |
-
"rules_evaluated":
|
| 42 |
"proposals_evaluated": 0,
|
| 43 |
-
"failures": 0,
|
| 44 |
-
"warnings": 0,
|
| 45 |
}
|
| 46 |
tracker.end_stage("validation")
|
| 47 |
return state
|
|
|
|
| 35 |
new_item_ids = {str(item.id) for item in new_items}
|
| 36 |
|
| 37 |
if not new_item_ids:
|
| 38 |
+
error_msg = state.metadata.get("extraction_error") or "No knowledge items were extracted from this document."
|
| 39 |
+
has_err = bool(state.metadata.get("extraction_error"))
|
| 40 |
+
state.validation_results = [
|
| 41 |
+
{
|
| 42 |
+
"status": "FAIL" if has_err else "WARNING",
|
| 43 |
+
"severity": "HIGH",
|
| 44 |
+
"rule_id": None,
|
| 45 |
+
"rule_name": "Extraction Verification",
|
| 46 |
+
"rule_type": None,
|
| 47 |
+
"operator": None,
|
| 48 |
+
"message": error_msg,
|
| 49 |
+
"proposal_ids": [],
|
| 50 |
+
}
|
| 51 |
+
]
|
| 52 |
state.metadata["validation_summary"] = {
|
| 53 |
+
"status": "FAIL" if has_err else "WARNING",
|
| 54 |
+
"rules_evaluated": 1,
|
| 55 |
"proposals_evaluated": 0,
|
| 56 |
+
"failures": 1 if has_err else 0,
|
| 57 |
+
"warnings": 0 if has_err else 1,
|
| 58 |
}
|
| 59 |
tracker.end_stage("validation")
|
| 60 |
return state
|
backend/tests/test_mcp_e2e.py
CHANGED
|
@@ -77,6 +77,7 @@ def test_mcp_e2e():
|
|
| 77 |
print(" PASS")
|
| 78 |
|
| 79 |
# 5. Get workflow status (use an existing workflow if available)
|
|
|
|
| 80 |
if result:
|
| 81 |
workflow_id = result[0]["workflow_id"]
|
| 82 |
print(f"\n5. get_workflow_status ({workflow_id[:8]}...)")
|
|
|
|
| 77 |
print(" PASS")
|
| 78 |
|
| 79 |
# 5. Get workflow status (use an existing workflow if available)
|
| 80 |
+
workflow_id = None
|
| 81 |
if result:
|
| 82 |
workflow_id = result[0]["workflow_id"]
|
| 83 |
print(f"\n5. get_workflow_status ({workflow_id[:8]}...)")
|
docs/{architecture-diagram.md β DocWeave_architecture_diagram.md}
RENAMED
|
File without changes
|
docs/one-pager.md
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
# DocWeave β One Page
|
| 2 |
-
|
| 3 |
-
## What I Built
|
| 4 |
-
|
| 5 |
-
DocWeave is an agentic document intelligence platform that transforms unstructured documents into a governed knowledge register. It processes PDFs through an AI pipeline (extract, chunk, embed, extract knowledge, reconcile, validate, decide) and either auto-commits verified knowledge or pauses for human review β depending on configurable validation rules.
|
| 6 |
-
|
| 7 |
-
Think of it as "GitHub for documents": every knowledge change is proposed, validated against rules, reviewed if needed, and committed with full audit trail.
|
| 8 |
-
|
| 9 |
-
## Who It's For
|
| 10 |
-
|
| 11 |
-
Teams that manage large document corpora and need structured, searchable, auditable knowledge β compliance officers, research groups, clinical teams, legal departments. Anyone who's ever said "I know we read this somewhere but can't find which document said it."
|
| 12 |
-
|
| 13 |
-
## Results
|
| 14 |
-
|
| 15 |
-
- **End-to-end processing**: Upload a PDF, get structured knowledge items (ENTITY, CLAIM, METHOD, METRIC, OBSERVATION) with verbatim evidence quotes and confidence scores in 20-45 seconds.
|
| 16 |
-
- **Zero-loss batch extraction**: Documents of any size are batched and processed sequentially β no data is truncated or lost.
|
| 17 |
-
- **Human-in-the-loop**: 4 configurable validation operators (min_confidence, required_evidence, allowed_proposal_types, topic_relevance) determine auto-commit vs. human review.
|
| 18 |
-
- **Crash resilience**: LangGraph checkpointing means workflows survive server restarts. Graceful shutdown waits for active work; auto-resume picks up orphaned workflows.
|
| 19 |
-
- **MCP integration**: 15-tool Model Context Protocol server lets AI agents drive the full lifecycle programmatically.
|
| 20 |
-
- **Deployed**: Frontend on Vercel, backend on HuggingFace Spaces (Docker), database on Neon PostgreSQL (Singapore).
|
| 21 |
-
|
| 22 |
-
## Key Trade-Offs
|
| 23 |
-
|
| 24 |
-
| Decision | Why |
|
| 25 |
-
|----------|-----|
|
| 26 |
-
| **Background threads (not Celery)** | Simpler deployment, no Redis/broker needed. Trade-off: limited to single-server horizontal scaling. Acceptable for a demo/small team tool. |
|
| 27 |
-
| **Groq free tier LLM** | Zero cost, fast inference. Trade-off: strict request size limits on free org tier β knowledge extraction requires small batches. A paid tier or self-hosted model removes this. |
|
| 28 |
-
| **Ephemeral file storage on HF** | Files don't persist across container restarts. Trade-off: retry on old docs fails. Acceptable because production would use S3/GCS. Demo works for freshly uploaded docs. |
|
| 29 |
-
| **sentence-transformers local embeddings** | No external API dependency, runs on CPU. Trade-off: ~300MB memory footprint, can't run on 512MB servers (Render free tier). Works fine on HF Spaces (16GB RAM). |
|
| 30 |
-
| **PostgreSQL for everything (vectors + relational)** | Single database, pgvector handles vector search. Trade-off: not as fast as dedicated vector DBs (Pinecone, Weaviate) at scale. Perfectly adequate for <100K documents. |
|
| 31 |
-
| **Native JSON mode (not tool calling)** | More reliable structured output from Groq/Qwen/Llama models. Trade-off: less strict schema enforcement than OpenAI function calling. Mitigated by robust JSON parsing with fallbacks. |
|
| 32 |
-
|
| 33 |
-
## What I'd Add With More Time
|
| 34 |
-
|
| 35 |
-
- Persistent cloud storage (S3) for uploaded documents
|
| 36 |
-
- WebSocket-based real-time progress (currently polls)
|
| 37 |
-
- Multi-user collaboration with role-based access
|
| 38 |
-
- Knowledge graph visualization (entity relationship map)
|
| 39 |
-
- Automatic conflict resolution using LLM reasoning
|
| 40 |
-
- PDF annotation overlay showing where each knowledge item was extracted from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|