studio / app /services /intelligence.py
Ava2lon's picture
Upload 46 files
3e93464 verified
Raw
History Blame
13.1 kB
import re
from copy import deepcopy
from typing import Any
from app.models.workflow import (
ContractResponse,
DependencyImpactResponse,
DocumentationResponse,
EnvironmentPromotionResponse,
IntentDriftResponse,
LineageField,
LineageResponse,
QualityResponse,
ReleasePlanRequest,
ReleasePlanResponse,
RoiResponse,
SelfHealResponse,
ValidationIssue,
WebhookInspectResponse,
WorkflowDocument,
WorkflowPackageRequest,
WorkflowPackageResponse,
)
from app.services.operations import estimate_cost
from app.services.optimizer import optimizer
from app.services.validation import validator
FIELD_REF = re.compile(r"\$json\.([A-Za-z_][\w.]*)")
ENV_REF = re.compile(r"\$\{ENV\.([A-Z][A-Z0-9_]*)\}")
WORD = re.compile(r"[a-z][a-z0-9_-]{2,}")
SECRET_NAMES = {"password", "secret", "token", "api_key", "apikey", "authorization"}
PERSONAL_NAMES = {"email", "phone", "address", "name", "ssn", "dob"}
FINANCIAL_NAMES = {"amount", "invoice", "card", "iban", "account", "price"}
STOP_WORDS = {"that", "with", "from", "this", "then", "into", "workflow", "create", "build"}
def _walk(value: Any, prefix: str = ""):
if isinstance(value, dict):
for key, nested in value.items():
path = f"{prefix}.{key}" if prefix else str(key)
yield path, nested
yield from _walk(nested, path)
elif isinstance(value, list):
for index, nested in enumerate(value):
yield from _walk(nested, f"{prefix}[{index}]")
def _classification(field: str) -> str:
parts = {part.lower() for part in re.split(r"[.\[\]_-]+", field) if part}
if parts & SECRET_NAMES:
return "secret"
if parts & FINANCIAL_NAMES:
return "financial"
if parts & PERSONAL_NAMES:
return "personal"
return "internal"
def lineage(workflow: WorkflowDocument) -> LineageResponse:
dependencies: dict[str, list[str]] = {node.id: [] for node in workflow.nodes}
fields: dict[str, dict[str, set[str] | str]] = {}
labels = {node.id: node.data.label for node in workflow.nodes}
for edge in workflow.edges:
if edge.target in dependencies and edge.source in labels:
dependencies[edge.target].append(labels[edge.source])
for node in workflow.nodes:
for path, value in _walk(node.data.parameters):
candidates = {path}
if isinstance(value, str):
candidates.update(FIELD_REF.findall(value))
for field in candidates:
record = fields.setdefault(
field,
{"sources": set(), "consumers": set(), "classification": _classification(field)},
)
if field == path:
record["sources"].add(node.data.label) # type: ignore[union-attr]
else:
record["consumers"].add(node.data.label) # type: ignore[union-attr]
result = [
LineageField(
field=field,
source_nodes=sorted(record["sources"]), # type: ignore[arg-type]
consumer_nodes=sorted(record["consumers"]), # type: ignore[arg-type]
classification=record["classification"], # type: ignore[arg-type]
)
for field, record in sorted(fields.items())
]
return LineageResponse(
fields=result,
node_dependencies=dependencies,
sensitive_paths=[field.field for field in result if field.classification in {"secret", "personal", "financial"}],
)
def _type_name(value: Any) -> str:
if value is None:
return "null"
if isinstance(value, bool):
return "boolean"
if isinstance(value, (int, float)):
return "number"
if isinstance(value, dict):
return "object"
if isinstance(value, list):
return "array"
return "string"
def contract_check(sample: dict[str, Any], expected: dict[str, str]) -> ContractResponse:
inferred = {path: _type_name(value) for path, value in _walk(sample) if not isinstance(value, (dict, list))}
violations = []
for path, expected_type in expected.items():
actual = inferred.get(path)
if actual is None:
violations.append(f"Missing required field: {path}")
elif actual != expected_type:
violations.append(f"{path} expected {expected_type}, received {actual}")
return ContractResponse(valid=not violations, inferred_schema=inferred, violations=violations)
def quality(workflow: WorkflowDocument) -> QualityResponse:
validation = validator.validate(workflow)
findings = list(validation.issues)
lineage_result = lineage(workflow)
code_nodes = [node for node in workflow.nodes if node.data.type == "n8n-nodes-base.code"]
if code_nodes:
findings.append(ValidationIssue(code="review_code", severity="warning", message="Code nodes require review and sandboxed testing."))
if lineage_result.sensitive_paths:
findings.append(ValidationIssue(code="sensitive_data", severity="warning", message="Sensitive fields require destination and retention review."))
retry_nodes = sum("retry" in str(node.data.parameters).lower() for node in workflow.nodes)
scores = {
"reliability": max(0, validation.score - (10 if retry_nodes == 0 else 0)),
"maintainability": max(0, 100 - len(code_nodes) * 12 - max(0, len(workflow.nodes) - 30)),
"security": max(0, 100 - len(lineage_result.sensitive_paths) * 4 - len(code_nodes) * 8),
"cost": max(0, 100 - sum("langchain" in node.data.type for node in workflow.nodes) * 7),
"observability": 90 if workflow.settings.get("saveExecutionProgress") else 55,
}
return QualityResponse(overall=round(sum(scores.values()) / len(scores)), scores=scores, findings=findings)
def intent_drift(workflow: WorkflowDocument, requirement: str) -> IntentDriftResponse:
terms = {term for term in WORD.findall(requirement.lower()) if term not in STOP_WORDS}
workflow_text = " ".join(
f"{node.data.label} {node.data.type} {node.data.subtitle or ''}" for node in workflow.nodes
).lower()
covered = sorted(term for term in terms if term in workflow_text)
missing = sorted(terms - set(covered))
return IntentDriftResponse(
alignment_score=round(len(covered) / max(1, len(terms)) * 100),
covered_terms=covered,
missing_terms=missing,
)
def promote(workflow: WorkflowDocument, environment: str, values: dict[str, Any]) -> EnvironmentPromotionResponse:
promoted = deepcopy(workflow)
replacements = 0
unresolved: set[str] = set()
def replace(value: Any) -> Any:
nonlocal replacements
if isinstance(value, str):
def substitute(match: re.Match[str]) -> str:
nonlocal replacements
key = match.group(1)
if key not in values:
unresolved.add(key)
return match.group(0)
replacements += 1
return str(values[key])
return ENV_REF.sub(substitute, value)
if isinstance(value, dict):
return {key: replace(nested) for key, nested in value.items()}
if isinstance(value, list):
return [replace(nested) for nested in value]
return value
for node in promoted.nodes:
node.data.parameters = replace(node.data.parameters)
promoted.meta.model_extra["environment"] = environment
return EnvironmentPromotionResponse(
workflow=promoted, environment=environment, replacements=replacements, unresolved=sorted(unresolved)
)
def release_plan(request: ReleasePlanRequest) -> ReleasePlanResponse:
validation = validator.validate(request.workflow)
blocked = not validation.valid
strategy_steps = {
"shadow": ["Mirror sanitized input to the candidate version", "Suppress side-effect nodes", "Compare schemas, outputs, and latency"],
"canary": [f"Route {request.traffic_percentage}% of eligible traffic to the candidate", "Monitor success rate and latency", "Increase traffic only after approval"],
"synthetic": ["Schedule representative test inputs", "Verify credentials and contracts", "Alert on consecutive failures"],
}
return ReleasePlanResponse(
strategy=request.strategy,
status="blocked" if blocked else "draft",
steps=strategy_steps[request.strategy],
rollback_conditions=[
f"Error rate exceeds {request.max_error_rate:.1%}",
f"Success rate drops below {request.success_threshold:.1%}",
"Output contract or sensitive-data policy fails",
],
warnings=[issue.message for issue in validation.issues if issue.severity == "error"],
)
def package_workflow(request: WorkflowPackageRequest) -> WorkflowPackageResponse:
return WorkflowPackageResponse(
manifest={
"format": "flowforge.workflow-package/v1",
"name": request.workflow.name,
"workflowVersion": request.workflow.meta.version or 1,
"n8nCompatible": True,
"contents": ["workflow", "tests", "contracts", "environments"],
},
workflow=request.workflow,
tests=request.tests,
contracts=request.contracts,
environments=request.environments,
)
def documentation(workflow: WorkflowDocument) -> DocumentationResponse:
nodes = "\n".join(f"- **{node.data.label}** (`{node.data.type}`): {node.data.subtitle or 'Configured action'}" for node in workflow.nodes)
connections = "\n".join(
f"- {next((n.data.label for n in workflow.nodes if n.id == edge.source), edge.source)} -> "
f"{next((n.data.label for n in workflow.nodes if n.id == edge.target), edge.target)}"
for edge in workflow.edges
)
return DocumentationResponse(markdown=f"# {workflow.name}\n\n{workflow.meta.description or 'n8n workflow'}\n\n## Nodes\n\n{nodes}\n\n## Connections\n\n{connections or '- None'}\n")
def roi(workflow: WorkflowDocument, executions: int, minutes_saved: float, hourly_rate: float, sla_minutes: float) -> RoiResponse:
hours = executions * minutes_saved / 60
cost = estimate_cost(workflow, executions).estimated_monthly_usd
estimated_duration = max(1, len(workflow.nodes) * 250)
return RoiResponse(
hours_saved=round(hours, 2), labor_value_usd=round(hours * hourly_rate, 2),
estimated_operating_cost_usd=cost, net_value_usd=round(hours * hourly_rate - cost, 2),
estimated_duration_ms=estimated_duration,
sla_headroom_percent=round(max(0, 1 - estimated_duration / (sla_minutes * 60_000)) * 100, 2),
)
def inspect_webhook(payload: dict[str, Any], redact: bool) -> WebhookInspectResponse:
result = deepcopy(payload)
schema = {path: _type_name(value) for path, value in _walk(payload) if not isinstance(value, (dict, list))}
redacted = []
if redact:
def scrub(value: Any, prefix: str = "") -> None:
if not isinstance(value, dict):
return
for key, nested in value.items():
path = f"{prefix}.{key}" if prefix else key
if _classification(path) in {"secret", "personal", "financial"}:
value[key] = "[REDACTED]"
redacted.append(path)
else:
scrub(nested, path)
scrub(result)
return WebhookInspectResponse(payload=result, schema_map=schema, redacted_fields=redacted)
def dependency_impact(workflow: WorkflowDocument, dependency: str) -> DependencyImpactResponse:
query = dependency.lower()
affected = [node for node in workflow.nodes if query in f"{node.data.type} {node.data.label} {node.data.parameters}".lower()]
affected_ids = {node.id for node in affected}
downstream_ids: set[str] = set()
frontier = list(affected_ids)
while frontier:
source = frontier.pop()
for edge in workflow.edges:
if edge.source == source and edge.target not in downstream_ids:
downstream_ids.add(edge.target)
frontier.append(edge.target)
labels = {node.id: node.data.label for node in workflow.nodes}
count = len(affected_ids | downstream_ids)
severity = "none" if count == 0 else "low" if count == 1 else "medium" if count < 5 else "high"
return DependencyImpactResponse(
affected_nodes=[node.data.label for node in affected],
downstream_nodes=[labels[node_id] for node_id in downstream_ids if node_id in labels],
severity=severity,
)
def self_heal(workflow: WorkflowDocument, errors: list[str]) -> SelfHealResponse:
before = quality(workflow)
optimized = optimizer.optimize(workflow)
after = quality(optimized.workflow)
changes = [suggestion.title for suggestion in optimized.suggestions]
if errors:
changes.append("Attached execution errors as approval evidence")
return SelfHealResponse(
proposed_workflow=optimized.workflow,
changes=changes,
quality_before=before.overall,
quality_after=after.overall,
)