forge-agent 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forge/__init__.py +63 -0
- forge/brain/__init__.py +4 -0
- forge/brain/approval.py +111 -0
- forge/brain/composer.py +54 -0
- forge/brain/contracts.py +128 -0
- forge/brain/council.py +244 -0
- forge/brain/intent.py +331 -0
- forge/brain/mission_store.py +186 -0
- forge/brain/operator.py +716 -0
- forge/brain/orchestrator.py +1541 -0
- forge/brain/planner.py +992 -0
- forge/brain/prompt.py +62 -0
- forge/brain/worker_executor.py +139 -0
- forge/brain/worker_protocol.py +56 -0
- forge/brain/worker_runtime.py +395 -0
- forge/cli/__init__.py +0 -0
- forge/cli/main.py +561 -0
- forge/config/__init__.py +3 -0
- forge/config/settings.py +63 -0
- forge/core/__init__.py +0 -0
- forge/core/discovery.py +291 -0
- forge/core/models.py +164 -0
- forge/core/quota.py +252 -0
- forge/core/router.py +560 -0
- forge/core/session.py +309 -0
- forge/desktop/__init__.py +2 -0
- forge/desktop/account_client.py +149 -0
- forge/desktop/app.py +371 -0
- forge/desktop/diagnostics.py +23 -0
- forge/desktop/runtime.py +1447 -0
- forge/desktop/server.py +3261 -0
- forge/memory/__init__.py +4 -0
- forge/memory/context.py +65 -0
- forge/memory/graph.py +351 -0
- forge/providers/__init__.py +8 -0
- forge/providers/anthropic.py +128 -0
- forge/providers/base.py +324 -0
- forge/providers/cloudflare.py +173 -0
- forge/providers/deepseek.py +129 -0
- forge/providers/gemini.py +135 -0
- forge/providers/groq.py +159 -0
- forge/providers/mistral.py +133 -0
- forge/providers/nvidia.py +145 -0
- forge/providers/ollama.py +264 -0
- forge/providers/openai.py +164 -0
- forge/providers/openrouter.py +139 -0
- forge/providers/registry.py +44 -0
- forge/providers/together.py +141 -0
- forge/recovery/__init__.py +3 -0
- forge/recovery/manager.py +47 -0
- forge/runtime/__init__.py +28 -0
- forge/runtime/agent.py +200 -0
- forge/runtime/contracts.py +59 -0
- forge/runtime/gateway.py +190 -0
- forge/runtime/heartbeat.py +108 -0
- forge/runtime/lanes.py +129 -0
- forge/runtime/markdown_memory.py +265 -0
- forge/runtime/state_store.py +877 -0
- forge/runtime/worker_host.py +207 -0
- forge/safety/__init__.py +3 -0
- forge/safety/guard.py +156 -0
- forge/safety/sanitizer.py +78 -0
- forge/skills/__init__.py +5 -0
- forge/skills/contracts.py +69 -0
- forge/skills/loader.py +129 -0
- forge/skills/registry.py +294 -0
- forge/skills/router.py +391 -0
- forge/skills/runtime.py +86 -0
- forge/skills_catalog/_template/SKILL.md +50 -0
- forge/skills_catalog/affiliate-offer-analyzer/SKILL.md +54 -0
- forge/skills_catalog/affiliate-offer-analyzer/executor.py +26 -0
- forge/skills_catalog/affiliate-offer-analyzer/schema.json +69 -0
- forge/skills_catalog/artifact-writer/SKILL.md +55 -0
- forge/skills_catalog/artifact-writer/executor.py +74 -0
- forge/skills_catalog/artifact-writer/schema.json +67 -0
- forge/skills_catalog/browser-executor/SKILL.md +65 -0
- forge/skills_catalog/browser-executor/executor.py +143 -0
- forge/skills_catalog/browser-executor/schema.json +133 -0
- forge/skills_catalog/codebase-analyzer/SKILL.md +64 -0
- forge/skills_catalog/codebase-analyzer/executor.py +230 -0
- forge/skills_catalog/codebase-analyzer/schema.json +67 -0
- forge/skills_catalog/external-publisher/SKILL.md +63 -0
- forge/skills_catalog/external-publisher/executor.py +54 -0
- forge/skills_catalog/external-publisher/schema.json +90 -0
- forge/skills_catalog/file-editor/SKILL.md +68 -0
- forge/skills_catalog/file-editor/executor.py +223 -0
- forge/skills_catalog/file-editor/schema.json +106 -0
- forge/skills_catalog/file-reader/SKILL.md +60 -0
- forge/skills_catalog/file-reader/executor.py +85 -0
- forge/skills_catalog/file-reader/schema.json +61 -0
- forge/skills_catalog/github-publisher/SKILL.md +71 -0
- forge/skills_catalog/github-publisher/executor.py +65 -0
- forge/skills_catalog/github-publisher/schema.json +97 -0
- forge/skills_catalog/research-brief/SKILL.md +54 -0
- forge/skills_catalog/research-brief/executor.py +23 -0
- forge/skills_catalog/research-brief/schema.json +68 -0
- forge/skills_catalog/seo-article-writer/SKILL.md +54 -0
- forge/skills_catalog/seo-article-writer/executor.py +24 -0
- forge/skills_catalog/seo-article-writer/schema.json +64 -0
- forge/skills_catalog/shell-executor/SKILL.md +65 -0
- forge/skills_catalog/shell-executor/executor.py +80 -0
- forge/skills_catalog/shell-executor/schema.json +84 -0
- forge/skills_catalog/system-inspector/SKILL.md +62 -0
- forge/skills_catalog/system-inspector/executor.py +93 -0
- forge/skills_catalog/system-inspector/schema.json +83 -0
- forge/skills_catalog/wordpress-publisher/SKILL.md +73 -0
- forge/skills_catalog/wordpress-publisher/executor.py +68 -0
- forge/skills_catalog/wordpress-publisher/schema.json +101 -0
- forge/skills_catalog/workspace-inspector/SKILL.md +55 -0
- forge/skills_catalog/workspace-inspector/executor.py +24 -0
- forge/skills_catalog/workspace-inspector/schema.json +72 -0
- forge/tools/__init__.py +15 -0
- forge/tools/browser.py +600 -0
- forge/tools/credentials.py +70 -0
- forge/tools/github.py +190 -0
- forge/tools/publish.py +50 -0
- forge/tools/shell.py +152 -0
- forge/tools/system.py +219 -0
- forge/tools/wordpress.py +159 -0
- forge/tools/workspace.py +422 -0
- forge/validation/__init__.py +11 -0
- forge/validation/json_validator.py +64 -0
- forge/validation/validator.py +273 -0
- forge_agent-1.1.5.dist-info/METADATA +255 -0
- forge_agent-1.1.5.dist-info/RECORD +129 -0
- forge_agent-1.1.5.dist-info/WHEEL +5 -0
- forge_agent-1.1.5.dist-info/entry_points.txt +2 -0
- forge_agent-1.1.5.dist-info/licenses/LICENSE +21 -0
- forge_agent-1.1.5.dist-info/top_level.txt +1 -0
forge/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FORGE — Free Open Reasoning & Generation Engine
|
|
3
|
+
================================================
|
|
4
|
+
The world's most powerful free AI agent.
|
|
5
|
+
Self-evolving. Zero cost. Forever.
|
|
6
|
+
|
|
7
|
+
forge.ask("write me a web scraper") # auto-selects best free model
|
|
8
|
+
forge.code("fix this bug", file="x.py") # routes to coding specialist
|
|
9
|
+
forge.research("quantum computing") # deep multi-model research
|
|
10
|
+
|
|
11
|
+
GitHub : https://github.com/trenstudio/forge
|
|
12
|
+
Website: https://www.trenstudio.com/FORGE
|
|
13
|
+
License: MIT
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "1.1.5"
|
|
17
|
+
__author__ = "TREN Studio"
|
|
18
|
+
__license__ = "MIT"
|
|
19
|
+
|
|
20
|
+
from forge.core.router import ForgeRouter
|
|
21
|
+
from forge.core.session import ForgeSession
|
|
22
|
+
from forge.brain.operator import ForgeOperator
|
|
23
|
+
from forge.memory.graph import MemoryGraph
|
|
24
|
+
from forge.runtime import ForgeAgentRuntime
|
|
25
|
+
|
|
26
|
+
# One-line convenience API
|
|
27
|
+
_default_session: ForgeSession | None = None
|
|
28
|
+
|
|
29
|
+
def _session() -> ForgeSession:
|
|
30
|
+
global _default_session
|
|
31
|
+
if _default_session is None:
|
|
32
|
+
_default_session = ForgeSession()
|
|
33
|
+
return _default_session
|
|
34
|
+
|
|
35
|
+
def ask(prompt: str, **kwargs) -> str:
|
|
36
|
+
"""Send a prompt. FORGE picks the best free model automatically."""
|
|
37
|
+
return _session().ask(prompt, **kwargs)
|
|
38
|
+
|
|
39
|
+
def code(prompt: str, **kwargs) -> str:
|
|
40
|
+
"""Coding task — routed to the strongest available coding model."""
|
|
41
|
+
return _session().ask(prompt, task_type="code", **kwargs)
|
|
42
|
+
|
|
43
|
+
def research(prompt: str, **kwargs) -> str:
|
|
44
|
+
"""Deep research task with optional web search."""
|
|
45
|
+
return _session().ask(prompt, task_type="research", **kwargs)
|
|
46
|
+
|
|
47
|
+
def operate(prompt: str, **kwargs) -> str:
|
|
48
|
+
"""Run the skill-based operator brain."""
|
|
49
|
+
operator = ForgeOperator()
|
|
50
|
+
return operator.handle_as_text(prompt, **kwargs)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"__version__",
|
|
54
|
+
"ForgeRouter",
|
|
55
|
+
"ForgeSession",
|
|
56
|
+
"ForgeOperator",
|
|
57
|
+
"ForgeAgentRuntime",
|
|
58
|
+
"MemoryGraph",
|
|
59
|
+
"ask",
|
|
60
|
+
"code",
|
|
61
|
+
"operate",
|
|
62
|
+
"research",
|
|
63
|
+
]
|
forge/brain/__init__.py
ADDED
forge/brain/approval.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from forge.runtime.state_store import PersistentStateStore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
AUTO_APPROVE_CLASSES = {"local_readonly"}
|
|
10
|
+
HUMAN_APPROVAL_CLASSES = {
|
|
11
|
+
"network_post",
|
|
12
|
+
"network_egress",
|
|
13
|
+
"authenticated_browser",
|
|
14
|
+
"external_publish",
|
|
15
|
+
"sensitive_file_write",
|
|
16
|
+
"destructive_shell",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class ApprovalDecision:
|
|
22
|
+
allowed: bool
|
|
23
|
+
approval_required: bool
|
|
24
|
+
approval_id: str | None
|
|
25
|
+
approval_class: str | None
|
|
26
|
+
notes: list[str]
|
|
27
|
+
status: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApprovalPolicyEngine:
|
|
31
|
+
"""Policy + encrypted payload approval flow."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, store: PersistentStateStore) -> None:
|
|
34
|
+
self._store = store
|
|
35
|
+
|
|
36
|
+
def evaluate(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
mission_id: str,
|
|
40
|
+
step_id: str,
|
|
41
|
+
approval_class: str | None,
|
|
42
|
+
request_excerpt: str,
|
|
43
|
+
payload: dict[str, Any],
|
|
44
|
+
summary: str,
|
|
45
|
+
confirmed: bool,
|
|
46
|
+
) -> ApprovalDecision:
|
|
47
|
+
if not approval_class:
|
|
48
|
+
return ApprovalDecision(True, False, None, None, [], "allowed")
|
|
49
|
+
|
|
50
|
+
if approval_class in AUTO_APPROVE_CLASSES:
|
|
51
|
+
return ApprovalDecision(
|
|
52
|
+
True,
|
|
53
|
+
False,
|
|
54
|
+
None,
|
|
55
|
+
approval_class,
|
|
56
|
+
[f"Approval class `{approval_class}` auto-approved by policy."],
|
|
57
|
+
"auto_approved",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if confirmed:
|
|
61
|
+
return ApprovalDecision(
|
|
62
|
+
True,
|
|
63
|
+
False,
|
|
64
|
+
None,
|
|
65
|
+
approval_class,
|
|
66
|
+
[f"Approval class `{approval_class}` approved explicitly by operator input."],
|
|
67
|
+
"confirmed",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if approval_class in HUMAN_APPROVAL_CLASSES:
|
|
71
|
+
approval_id = self._store.create_pending_approval(
|
|
72
|
+
mission_id=mission_id,
|
|
73
|
+
step_id=step_id,
|
|
74
|
+
approval_class=approval_class,
|
|
75
|
+
request_excerpt=request_excerpt[:500],
|
|
76
|
+
payload=payload,
|
|
77
|
+
summary=summary,
|
|
78
|
+
policy_mode="human_approval",
|
|
79
|
+
)
|
|
80
|
+
return ApprovalDecision(
|
|
81
|
+
False,
|
|
82
|
+
True,
|
|
83
|
+
approval_id,
|
|
84
|
+
approval_class,
|
|
85
|
+
[
|
|
86
|
+
f"Approval class: {approval_class}. Human confirmation is required before execution.",
|
|
87
|
+
f"Approval request created: {approval_id}.",
|
|
88
|
+
],
|
|
89
|
+
"pending",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return ApprovalDecision(
|
|
93
|
+
True,
|
|
94
|
+
False,
|
|
95
|
+
None,
|
|
96
|
+
approval_class,
|
|
97
|
+
[f"Approval class `{approval_class}` allowed by current policy."],
|
|
98
|
+
"allowed",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def approval_status(self, approval_id: str) -> dict[str, Any] | None:
|
|
102
|
+
return self._store.get_approval(approval_id, include_payload=False)
|
|
103
|
+
|
|
104
|
+
def approve(self, approval_id: str, *, notes: str = "") -> dict[str, Any] | None:
|
|
105
|
+
return self._store.decide_approval(approval_id, approved=True, notes=notes)
|
|
106
|
+
|
|
107
|
+
def reject(self, approval_id: str, *, notes: str = "") -> dict[str, Any] | None:
|
|
108
|
+
return self._store.decide_approval(approval_id, approved=False, notes=notes)
|
|
109
|
+
|
|
110
|
+
def list_pending(self) -> list[dict[str, Any]]:
|
|
111
|
+
return self._store.list_approvals(status="pending")
|
forge/brain/composer.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from forge.brain.contracts import CompletionState, IntentKind, OperatorResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ResponseComposer:
|
|
7
|
+
"""Compose the final operator-grade response."""
|
|
8
|
+
|
|
9
|
+
def compose(self, result: OperatorResult) -> str:
|
|
10
|
+
if not result.step_results and result.intent.primary_intent.value == "conversation":
|
|
11
|
+
return result.result.strip()
|
|
12
|
+
|
|
13
|
+
if not result.step_results and result.validation_status == CompletionState.FINISHED:
|
|
14
|
+
return result.result.strip()
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
result.validation_status == CompletionState.FINISHED
|
|
18
|
+
and result.intent.primary_intent in {IntentKind.RESEARCH, IntentKind.ANALYSIS}
|
|
19
|
+
and not result.risks_or_limitations
|
|
20
|
+
):
|
|
21
|
+
return result.result.strip()
|
|
22
|
+
|
|
23
|
+
validation = result.validation_status.value
|
|
24
|
+
risks = result.risks_or_limitations or []
|
|
25
|
+
completed = sum(1 for step in result.step_results if step.status == CompletionState.FINISHED)
|
|
26
|
+
total = len(result.step_results)
|
|
27
|
+
|
|
28
|
+
lines = [result.result.strip()]
|
|
29
|
+
if total:
|
|
30
|
+
lines.extend(
|
|
31
|
+
[
|
|
32
|
+
"",
|
|
33
|
+
f"Status: {validation}. Steps completed: {completed}/{total}.",
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
lines.extend(["", f"Status: {validation}."])
|
|
38
|
+
if risks:
|
|
39
|
+
lines.extend(["", "Limitations:"])
|
|
40
|
+
lines.extend(f"- {item}" for item in risks[:4])
|
|
41
|
+
if result.best_next_action:
|
|
42
|
+
lines.extend(["", f"Next: {result.best_next_action}"])
|
|
43
|
+
return "\n".join(line for line in lines if line is not None)
|
|
44
|
+
|
|
45
|
+
def best_next_action(self, status: CompletionState) -> str:
|
|
46
|
+
if status == CompletionState.FINISHED:
|
|
47
|
+
return "Proceed to the next business action or persist the artifact."
|
|
48
|
+
if status == CompletionState.PARTIALLY_FINISHED:
|
|
49
|
+
return "Review the partial output, then rerun the blocked step with tighter inputs."
|
|
50
|
+
if status == CompletionState.NEEDS_HUMAN_CONFIRMATION:
|
|
51
|
+
return "Review the risk note and provide explicit confirmation before execution."
|
|
52
|
+
if status == CompletionState.NEEDS_RETRY:
|
|
53
|
+
return "Retry the failed step or switch to the suggested fallback skill."
|
|
54
|
+
return "Inspect the failure reason, adjust inputs, and rerun safely."
|
forge/brain/contracts.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IntentKind(str, Enum):
|
|
10
|
+
RESEARCH = "research"
|
|
11
|
+
WRITING = "writing"
|
|
12
|
+
TRANSFORMATION = "transformation"
|
|
13
|
+
PUBLISHING = "publishing"
|
|
14
|
+
AUTOMATION = "automation"
|
|
15
|
+
ORCHESTRATION = "orchestration"
|
|
16
|
+
DEBUGGING = "debugging"
|
|
17
|
+
ANALYSIS = "analysis"
|
|
18
|
+
CONTENT_GENERATION = "content_generation"
|
|
19
|
+
STRUCTURED_OUTPUT = "structured_output"
|
|
20
|
+
CONVERSATION = "conversation"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExecutionClass(str, Enum):
|
|
24
|
+
SIMPLE_REASONING = "simple_reasoning"
|
|
25
|
+
SINGLE_SKILL = "single_skill"
|
|
26
|
+
MULTI_SKILL_PIPELINE = "multi_skill_pipeline"
|
|
27
|
+
RISKY_ACTION = "risky_action"
|
|
28
|
+
REQUIRES_CONFIRMATION = "requires_confirmation"
|
|
29
|
+
REQUIRES_VALIDATION = "requires_validation"
|
|
30
|
+
REQUIRES_FALLBACK = "requires_fallback"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RiskLevel(str, Enum):
|
|
34
|
+
LOW = "low"
|
|
35
|
+
MEDIUM = "medium"
|
|
36
|
+
HIGH = "high"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CompletionState(str, Enum):
|
|
40
|
+
FINISHED = "finished"
|
|
41
|
+
PARTIALLY_FINISHED = "partially_finished"
|
|
42
|
+
FAILED = "failed"
|
|
43
|
+
NEEDS_RETRY = "needs_retry"
|
|
44
|
+
NEEDS_HUMAN_CONFIRMATION = "needs_human_confirmation"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TaskIntent(BaseModel):
|
|
48
|
+
raw_request: str
|
|
49
|
+
objective: str
|
|
50
|
+
primary_intent: IntentKind
|
|
51
|
+
intents: list[IntentKind] = Field(default_factory=list)
|
|
52
|
+
task_type: str = "general"
|
|
53
|
+
hidden_intent: str = ""
|
|
54
|
+
requested_output: str = "clean final response"
|
|
55
|
+
execution_classes: list[ExecutionClass] = Field(default_factory=list)
|
|
56
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
57
|
+
notes: list[str] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PlanStep(BaseModel):
|
|
61
|
+
id: str
|
|
62
|
+
action: str
|
|
63
|
+
skill: str | None = None
|
|
64
|
+
tool: str | None = None
|
|
65
|
+
input_spec: dict[str, Any] = Field(default_factory=dict)
|
|
66
|
+
expected_output: str
|
|
67
|
+
validation: str
|
|
68
|
+
risk_note: str = ""
|
|
69
|
+
fallback_skill: str | None = None
|
|
70
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
71
|
+
retry_limit: int = 2
|
|
72
|
+
stop_on_failure: bool = True
|
|
73
|
+
rollback_on_failure: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ExecutionPlan(BaseModel):
|
|
77
|
+
objective: str
|
|
78
|
+
task_type: str
|
|
79
|
+
risk_level: RiskLevel
|
|
80
|
+
steps: list[PlanStep] = Field(default_factory=list)
|
|
81
|
+
fallbacks: list[str] = Field(default_factory=list)
|
|
82
|
+
completion_criteria: list[str] = Field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class StepExecutionResult(BaseModel):
|
|
86
|
+
step_id: str
|
|
87
|
+
skill: str | None = None
|
|
88
|
+
tool: str | None = None
|
|
89
|
+
status: CompletionState
|
|
90
|
+
output: Any = None
|
|
91
|
+
evidence: list[str] = Field(default_factory=list)
|
|
92
|
+
validation_status: CompletionState = CompletionState.FAILED
|
|
93
|
+
validation_notes: list[str] = Field(default_factory=list)
|
|
94
|
+
attempts: int = 1
|
|
95
|
+
input_snapshot: dict[str, Any] = Field(default_factory=dict)
|
|
96
|
+
trace: list[str] = Field(default_factory=list)
|
|
97
|
+
rolled_back: bool = False
|
|
98
|
+
rollback_notes: list[str] = Field(default_factory=list)
|
|
99
|
+
agent_reviews: list[str] = Field(default_factory=list)
|
|
100
|
+
error: str = ""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AgentReview(BaseModel):
|
|
104
|
+
agent: str
|
|
105
|
+
status: CompletionState
|
|
106
|
+
notes: list[str] = Field(default_factory=list)
|
|
107
|
+
confidence: float | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class OperatorResult(BaseModel):
|
|
111
|
+
objective: str
|
|
112
|
+
approach_taken: list[str] = Field(default_factory=list)
|
|
113
|
+
result: str
|
|
114
|
+
user_response: str = ""
|
|
115
|
+
technical_details: dict[str, Any] = Field(default_factory=dict)
|
|
116
|
+
validation_status: CompletionState
|
|
117
|
+
risks_or_limitations: list[str] = Field(default_factory=list)
|
|
118
|
+
best_next_action: str
|
|
119
|
+
intent: TaskIntent
|
|
120
|
+
plan: ExecutionPlan
|
|
121
|
+
step_results: list[StepExecutionResult] = Field(default_factory=list)
|
|
122
|
+
artifacts: dict[str, Any] = Field(default_factory=dict)
|
|
123
|
+
mission_trace: list[str] = Field(default_factory=list)
|
|
124
|
+
mission_id: str = ""
|
|
125
|
+
audit_log_path: str = ""
|
|
126
|
+
resumed_from_step: str | None = None
|
|
127
|
+
agent_reviews: list[AgentReview] = Field(default_factory=list)
|
|
128
|
+
provider_telemetry: dict[str, Any] = Field(default_factory=dict)
|
forge/brain/council.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from forge.brain.contracts import AgentReview, CompletionState, ExecutionPlan, StepExecutionResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
RESEARCH_TERMS = {
|
|
10
|
+
"research",
|
|
11
|
+
"analyze",
|
|
12
|
+
"analysis",
|
|
13
|
+
"summarize",
|
|
14
|
+
"summary",
|
|
15
|
+
"verify",
|
|
16
|
+
"verification",
|
|
17
|
+
"compare",
|
|
18
|
+
"find",
|
|
19
|
+
"inspect",
|
|
20
|
+
"report",
|
|
21
|
+
"brief",
|
|
22
|
+
"researching",
|
|
23
|
+
"تحليل",
|
|
24
|
+
"ابحث",
|
|
25
|
+
"بحث",
|
|
26
|
+
"قارن",
|
|
27
|
+
"تحقق",
|
|
28
|
+
"لخص",
|
|
29
|
+
"تقرير",
|
|
30
|
+
}
|
|
31
|
+
STOPWORDS = {
|
|
32
|
+
"the",
|
|
33
|
+
"and",
|
|
34
|
+
"then",
|
|
35
|
+
"with",
|
|
36
|
+
"from",
|
|
37
|
+
"into",
|
|
38
|
+
"that",
|
|
39
|
+
"this",
|
|
40
|
+
"your",
|
|
41
|
+
"page",
|
|
42
|
+
"file",
|
|
43
|
+
"save",
|
|
44
|
+
"open",
|
|
45
|
+
"click",
|
|
46
|
+
"fill",
|
|
47
|
+
"extract",
|
|
48
|
+
"run",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ResearchAgent:
|
|
53
|
+
"""Prepare browser-heavy missions and convert raw snapshots into verified findings."""
|
|
54
|
+
|
|
55
|
+
def prepare_step(self, request: str, step, payload: dict[str, Any]) -> list[str]:
|
|
56
|
+
notes: list[str] = []
|
|
57
|
+
if step.skill == "browser-executor" and self._is_research_request(request):
|
|
58
|
+
notes.append("Research agent marked this browser step as evidence-first.")
|
|
59
|
+
if step.skill == "file-editor" and "content" not in payload:
|
|
60
|
+
notes.append("Research agent expects file content to be grounded in prior evidence.")
|
|
61
|
+
return notes
|
|
62
|
+
|
|
63
|
+
def enrich_output(self, request: str, step, output: Any) -> tuple[Any, AgentReview | None]:
|
|
64
|
+
if step.skill != "browser-executor" or not isinstance(output, dict):
|
|
65
|
+
return output, None
|
|
66
|
+
if not self._is_research_request(request):
|
|
67
|
+
return output, None
|
|
68
|
+
|
|
69
|
+
enriched = dict(output)
|
|
70
|
+
summary_markdown, confidence, verification = self._summarize_browser_output(request, enriched)
|
|
71
|
+
enriched["research_summary_markdown"] = summary_markdown
|
|
72
|
+
enriched["confidence"] = confidence
|
|
73
|
+
enriched["verification"] = verification
|
|
74
|
+
|
|
75
|
+
evidence = list(enriched.get("evidence", []))
|
|
76
|
+
evidence.append(f"confidence:{confidence:.2f}")
|
|
77
|
+
if verification.get("request_overlap_terms"):
|
|
78
|
+
evidence.append(
|
|
79
|
+
"overlap:" + ", ".join(verification["request_overlap_terms"][:6])
|
|
80
|
+
)
|
|
81
|
+
enriched["evidence"] = list(dict.fromkeys(evidence))
|
|
82
|
+
|
|
83
|
+
status = CompletionState.FINISHED if verification.get("verified") else CompletionState.PARTIALLY_FINISHED
|
|
84
|
+
review = AgentReview(
|
|
85
|
+
agent="research",
|
|
86
|
+
status=status,
|
|
87
|
+
notes=[
|
|
88
|
+
"Research chain completed: navigate -> snapshot -> summarize -> verify.",
|
|
89
|
+
f"Confidence score: {confidence:.2f}.",
|
|
90
|
+
],
|
|
91
|
+
confidence=confidence,
|
|
92
|
+
)
|
|
93
|
+
return enriched, review
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _is_research_request(request: str) -> bool:
|
|
97
|
+
lowered = request.lower()
|
|
98
|
+
return any(term in lowered for term in RESEARCH_TERMS)
|
|
99
|
+
|
|
100
|
+
def _summarize_browser_output(self, request: str, output: dict[str, Any]) -> tuple[str, float, dict[str, Any]]:
|
|
101
|
+
page_state = output.get("page_state") or {}
|
|
102
|
+
headings = [self._entry_text(item) for item in page_state.get("headings", []) if isinstance(item, dict)]
|
|
103
|
+
links = [self._entry_text(item) for item in page_state.get("links", []) if isinstance(item, dict)]
|
|
104
|
+
texts = [self._entry_text(item) for item in page_state.get("text", []) if isinstance(item, dict)]
|
|
105
|
+
|
|
106
|
+
request_terms = self._request_terms(request)
|
|
107
|
+
page_terms = self._request_terms(" ".join(headings + texts + links))
|
|
108
|
+
overlap = sorted(request_terms.intersection(page_terms))
|
|
109
|
+
semantic_count = sum(len(value) for value in page_state.values() if isinstance(value, list))
|
|
110
|
+
|
|
111
|
+
confidence = 0.35
|
|
112
|
+
if output.get("current_url"):
|
|
113
|
+
confidence += 0.15
|
|
114
|
+
if semantic_count >= 4:
|
|
115
|
+
confidence += 0.2
|
|
116
|
+
elif semantic_count >= 1:
|
|
117
|
+
confidence += 0.1
|
|
118
|
+
if overlap:
|
|
119
|
+
confidence += min(0.2, 0.04 * len(overlap))
|
|
120
|
+
if str(output.get("snapshot_text", "")).strip():
|
|
121
|
+
confidence += 0.1
|
|
122
|
+
confidence = max(0.15, min(0.95, confidence))
|
|
123
|
+
|
|
124
|
+
verified = bool(output.get("current_url")) and semantic_count > 0
|
|
125
|
+
lines = [
|
|
126
|
+
"# Browser Research Summary",
|
|
127
|
+
f"- Source URL: {output.get('current_url', '') or 'unknown'}",
|
|
128
|
+
f"- Confidence: {confidence:.2f}",
|
|
129
|
+
f"- Verified: {'yes' if verified else 'no'}",
|
|
130
|
+
]
|
|
131
|
+
if headings:
|
|
132
|
+
lines.append(f"- Headings: {', '.join(headings[:4])}")
|
|
133
|
+
if texts:
|
|
134
|
+
lines.append(f"- Key text: {', '.join(texts[:4])}")
|
|
135
|
+
if links:
|
|
136
|
+
lines.append(f"- Links: {', '.join(links[:4])}")
|
|
137
|
+
if overlap:
|
|
138
|
+
lines.append(f"- Request overlap: {', '.join(overlap[:8])}")
|
|
139
|
+
|
|
140
|
+
verification = {
|
|
141
|
+
"verified": verified,
|
|
142
|
+
"source_url": output.get("current_url", ""),
|
|
143
|
+
"semantic_nodes": semantic_count,
|
|
144
|
+
"request_overlap_terms": overlap[:10],
|
|
145
|
+
}
|
|
146
|
+
return "\n".join(lines), confidence, verification
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _request_terms(text: str) -> set[str]:
|
|
150
|
+
return {
|
|
151
|
+
token
|
|
152
|
+
for token in re.findall(r"[a-zA-Z0-9_-]{3,}", text.lower())
|
|
153
|
+
if token not in STOPWORDS
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _entry_text(entry: dict[str, Any]) -> str:
|
|
158
|
+
for key in ("name", "value", "description", "label"):
|
|
159
|
+
value = str(entry.get(key, "")).strip()
|
|
160
|
+
if value:
|
|
161
|
+
return value
|
|
162
|
+
return ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ActionAgent:
|
|
166
|
+
"""Owns execution dispatch and keeps the mission trace explicit."""
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def dispatch_notes(step) -> list[str]:
|
|
170
|
+
tool_name = step.tool or step.skill or "reasoning"
|
|
171
|
+
return [f"Action agent executing `{tool_name}`."]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class CriticAgent:
|
|
175
|
+
"""Reviews each step and the final mission before the user sees it."""
|
|
176
|
+
|
|
177
|
+
def review_step(self, request: str, step, output: Any, validation_status: CompletionState) -> AgentReview:
|
|
178
|
+
notes: list[str] = []
|
|
179
|
+
confidence: float | None = None
|
|
180
|
+
status = validation_status
|
|
181
|
+
|
|
182
|
+
if isinstance(output, dict):
|
|
183
|
+
confidence_value = output.get("confidence")
|
|
184
|
+
if isinstance(confidence_value, (int, float)):
|
|
185
|
+
confidence = float(confidence_value)
|
|
186
|
+
if confidence < 0.55 and status == CompletionState.FINISHED:
|
|
187
|
+
status = CompletionState.PARTIALLY_FINISHED
|
|
188
|
+
notes.append("Critic downgraded this result because confidence is too low.")
|
|
189
|
+
|
|
190
|
+
verification = output.get("verification")
|
|
191
|
+
if isinstance(verification, dict) and not verification.get("verified", True):
|
|
192
|
+
if status == CompletionState.FINISHED:
|
|
193
|
+
status = CompletionState.PARTIALLY_FINISHED
|
|
194
|
+
notes.append("Critic could not fully verify the browser-derived output.")
|
|
195
|
+
|
|
196
|
+
if step.skill == "file-editor" and output.get("changed") is False:
|
|
197
|
+
if status == CompletionState.FINISHED:
|
|
198
|
+
status = CompletionState.PARTIALLY_FINISHED
|
|
199
|
+
notes.append("Critic detected that the file edit produced no change.")
|
|
200
|
+
|
|
201
|
+
if step.skill == "shell-executor" and output.get("exit_code") == 0 and not output.get("stdout") and not output.get("stderr"):
|
|
202
|
+
notes.append("Critic accepted the shell step but flagged minimal observable output.")
|
|
203
|
+
|
|
204
|
+
if not notes:
|
|
205
|
+
notes.append("Critic accepted the step output.")
|
|
206
|
+
return AgentReview(agent="critic", status=status, notes=notes, confidence=confidence)
|
|
207
|
+
|
|
208
|
+
def review_mission(self, plan: ExecutionPlan, step_results: list[StepExecutionResult]) -> AgentReview:
|
|
209
|
+
if not step_results:
|
|
210
|
+
return AgentReview(
|
|
211
|
+
agent="critic",
|
|
212
|
+
status=CompletionState.FAILED,
|
|
213
|
+
notes=["Mission produced no step results."],
|
|
214
|
+
confidence=0.0,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
statuses = {step.status for step in step_results}
|
|
218
|
+
if CompletionState.FAILED in statuses:
|
|
219
|
+
return AgentReview(
|
|
220
|
+
agent="critic",
|
|
221
|
+
status=CompletionState.PARTIALLY_FINISHED if CompletionState.FINISHED in statuses else CompletionState.FAILED,
|
|
222
|
+
notes=["Mission ended with at least one failed step."],
|
|
223
|
+
confidence=0.45,
|
|
224
|
+
)
|
|
225
|
+
if CompletionState.PARTIALLY_FINISHED in statuses:
|
|
226
|
+
return AgentReview(
|
|
227
|
+
agent="critic",
|
|
228
|
+
status=CompletionState.PARTIALLY_FINISHED,
|
|
229
|
+
notes=["Mission completed, but one or more steps remain partial."],
|
|
230
|
+
confidence=0.65,
|
|
231
|
+
)
|
|
232
|
+
if len(step_results) < len(plan.steps):
|
|
233
|
+
return AgentReview(
|
|
234
|
+
agent="critic",
|
|
235
|
+
status=CompletionState.PARTIALLY_FINISHED,
|
|
236
|
+
notes=["Mission stopped before every planned step executed."],
|
|
237
|
+
confidence=0.55,
|
|
238
|
+
)
|
|
239
|
+
return AgentReview(
|
|
240
|
+
agent="critic",
|
|
241
|
+
status=CompletionState.FINISHED,
|
|
242
|
+
notes=["Mission passed the final critic review."],
|
|
243
|
+
confidence=0.85,
|
|
244
|
+
)
|