File size: 4,007 Bytes
81ff144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import re
import logging
from typing import List, Dict, Any
from services.supabase_service import supabase

logger = logging.getLogger("uvicorn")

class SemanticBackpropService:
    """
    Ensures numerical consistency across agent tasks by extracting 'Canonical Numbers'
    from previous task outputs.
    """

    @staticmethod
    async def get_project_context(project_id: str, current_task_id: str) -> str:
        """
        Fetches and extracts canonical figures from all completed sibling tasks.
        """
        try:
            resp = supabase.table("tasks") \
                .select("title, output_data") \
                .eq("project_id", project_id) \
                .eq("status", "done") \
                .neq("id", current_task_id) \
                .execute()
            
            if not resp.data:
                return ""

            canonical_blocks = []
            topic_blocks = []

            for task in resp.data:
                output = task.get("output_data") or {}
                # Handle different output formats (raw string or dict with 'result')
                result_text = ""
                if isinstance(output, dict):
                    result_text = output.get("result", "") or output.get("raw_output", "")
                elif isinstance(output, str):
                    result_text = output

                if not result_text:
                    continue

                # Extract financial and numerical lines
                lines = result_text.splitlines()
                financial_lines = []
                
                # Keywords that often indicate a 'canonical' number
                keywords = [
                    "$", "%", "USD", "MRR", "ARR", "ROI", "cost", "budget", 
                    "revenue", "price", "fee", "estimate", "total", "quota"
                ]

                for line in lines:
                    if any(k.lower() in line.lower() for k in keywords):
                        if len(line.strip()) > 5: # Ignore very short lines
                            financial_lines.append(line.strip())

                if financial_lines:
                    # De-duplicate similar lines
                    seen = set()
                    unique_fin = []
                    for fl in financial_lines:
                        key = fl[:50]
                        if key not in seen:
                            seen.add(key)
                            unique_fin.append(fl)

                    canonical_blocks.append(
                        f"Source Task: **{task['title']}**\n" +
                        "\n".join(f"  • {fl}" for fl in unique_fin[:8])
                    )

                # Also track what topics were covered to avoid repetition
                topic_blocks.append(f"- **{task['title']}**: (Covered in previous step)")

            if not canonical_blocks and not topic_blocks:
                return ""

            context = "\n---\n"
            if canonical_blocks:
                context += (
                    "### ⚠️ CANONICAL FIGURES — PREVIOUSLY ESTABLISHED\n"
                    "> **MANDATORY RULE**: The following numbers and figures were established by agents\n"
                    "> responsible for those domains. You MUST use these exact values if you reference them.\n"
                    "> DO NOT re-calculate or propose alternative values for these specific items.\n\n"
                )
                context += "\n\n".join(canonical_blocks) + "\n\n"
            
            if topic_blocks:
                context += (
                    "### 📋 PREVIOUSLY COVERED TOPICS\n"
                    "> Do not repeat the analysis of these topics. Focus only on your specific task.\n"
                )
                context += "\n".join(topic_blocks) + "\n"

            return context

        except Exception as e:
            logger.error(f"Semantic Backprop failed: {e}")
            return ""

semantic_backprop = SemanticBackpropService()