Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.

Commit 2da5485

Browse files
committed
feat: score discovery with manifest intent and stage metadata
1 parent a685fd5 commit 2da5485

File tree

2 files changed

+242
-41
lines changed

2 files changed

+242
-41
lines changed

src/agenticflow_cli/main.py

Lines changed: 216 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -648,21 +648,30 @@ def _catalog_operation_item(operation: Any) -> dict[str, Any]:
648648

649649

650650
@lru_cache(maxsize=1)
651-
def _manifest_scope_by_operation_id() -> dict[str, str]:
651+
def _manifest_metadata_by_operation_id() -> dict[str, dict[str, Any]]:
652652
try:
653653
raw = json.loads(CURATED_MANIFEST_PATH.read_text(encoding="utf-8"))
654654
except Exception:
655655
return {}
656656
if not isinstance(raw, list):
657657
return {}
658658

659-
scopes: dict[str, str] = {}
659+
records: dict[str, dict[str, Any]] = {}
660660
for item in raw:
661661
if not isinstance(item, Mapping):
662662
continue
663663
operation_id = item.get("operation_id")
664-
support_scope = item.get("support_scope")
665-
if isinstance(operation_id, str) and operation_id and isinstance(support_scope, str):
664+
if not isinstance(operation_id, str) or not operation_id:
665+
continue
666+
records[operation_id] = dict(item)
667+
return records
668+
669+
670+
def _manifest_scope_by_operation_id() -> dict[str, str]:
671+
scopes: dict[str, str] = {}
672+
for operation_id, metadata in _manifest_metadata_by_operation_id().items():
673+
support_scope = metadata.get("support_scope")
674+
if isinstance(support_scope, str):
666675
scopes[operation_id] = support_scope
667676
return scopes
668677

@@ -685,17 +694,125 @@ def _apply_curated_manifest_filter(
685694
if not _should_use_curated_manifest(spec_file, public_only):
686695
return operations
687696

688-
manifest_scope = _manifest_scope_by_operation_id()
689-
if not manifest_scope:
697+
manifest_metadata = _manifest_metadata_by_operation_id()
698+
if not manifest_metadata:
690699
return operations
691-
allowed_operation_ids = set(manifest_scope)
700+
allowed_operation_ids = set(manifest_metadata)
692701
return [
693702
operation
694703
for operation in operations
695704
if getattr(operation, "operation_id", None) in allowed_operation_ids
696705
]
697706

698707

708+
def _normalize_manifest_intents(value: Any) -> set[str]:
709+
intents: set[str] = set()
710+
if isinstance(value, str):
711+
cleaned = value.strip()
712+
if cleaned:
713+
intents.add(cleaned)
714+
return intents
715+
if isinstance(value, list):
716+
for item in value:
717+
if isinstance(item, str):
718+
cleaned = item.strip()
719+
if cleaned:
720+
intents.add(cleaned)
721+
return intents
722+
723+
724+
def _collect_manifest_intents(metadata: Mapping[str, Any]) -> set[str]:
725+
intents = _normalize_manifest_intents(metadata.get("intent"))
726+
intents.update(_normalize_manifest_intents(metadata.get("intents")))
727+
return intents
728+
729+
730+
def _normalize_manifest_dependencies(value: Any) -> tuple[str, ...]:
731+
dependencies: list[str] = []
732+
if isinstance(value, list):
733+
for item in value:
734+
if isinstance(item, str) and item:
735+
dependencies.append(item)
736+
return tuple(sorted(set(dependencies)))
737+
738+
739+
def _infer_task_intents(task_terms: set[str]) -> set[str]:
740+
inferred: set[str] = set()
741+
if task_terms.intersection({"agent", "agents", "assistant"}):
742+
inferred.add("build_agent")
743+
if task_terms.intersection({"workflow", "workflows", "flow", "automation"}):
744+
inferred.add("build_workflow")
745+
if task_terms.intersection({"workforce", "workforces", "mas", "team"}):
746+
inferred.add("build_workforce")
747+
if task_terms.intersection({"run", "execute", "trigger", "invoke", "launch"}):
748+
inferred.add("run")
749+
if task_terms.intersection(
750+
{"debug", "inspect", "status", "error", "errors", "trace", "validate", "diagnose"}
751+
):
752+
inferred.add("debug")
753+
if "build" in task_terms and not inferred:
754+
inferred.add("build_workflow")
755+
return inferred
756+
757+
758+
def _intent_bonus_for_operation(
759+
operation_intents: set[str],
760+
inferred_task_intents: set[str],
761+
) -> float:
762+
if not operation_intents or not inferred_task_intents:
763+
return 0.0
764+
if operation_intents.intersection(inferred_task_intents):
765+
return 5.0
766+
return 0.0
767+
768+
769+
def _stage_bonus_for_operation(
770+
*,
771+
stage: str | None,
772+
task_terms: set[str],
773+
inferred_task_intents: set[str],
774+
) -> float:
775+
if not stage:
776+
return 0.0
777+
778+
stage = stage.strip().lower()
779+
if not stage:
780+
return 0.0
781+
782+
wants_dependency_context = bool(
783+
task_terms.intersection(
784+
{"build", "dependency", "dependencies", "configure", "design", "plan"}
785+
)
786+
)
787+
wants_apply = bool(task_terms.intersection({"create", "update", "save"}))
788+
789+
if "run" in inferred_task_intents:
790+
if stage in {"apply", "observe"}:
791+
return 2.0
792+
return 0.0
793+
794+
if "debug" in inferred_task_intents:
795+
if stage in {"observe", "validate"}:
796+
return 2.0
797+
return 0.5 if stage == "discover" else 0.0
798+
799+
if inferred_task_intents.intersection({"build_workflow", "build_agent", "build_workforce"}):
800+
if wants_dependency_context:
801+
return {
802+
"discover": 2.5,
803+
"validate": 2.0,
804+
"apply": 1.0,
805+
"observe": 0.5,
806+
}.get(stage, 0.0)
807+
if wants_apply:
808+
return {
809+
"apply": 2.0,
810+
"validate": 1.0,
811+
"discover": 0.5,
812+
}.get(stage, 0.0)
813+
return 0.0
814+
815+
699816
def _catalog_records(
700817
registry: OperationRegistry,
701818
public_only: bool,
@@ -779,11 +896,33 @@ def _rank_catalog_operations(
779896
max_cost: float | None = None,
780897
max_latency_ms: float | None = None,
781898
manifest_scope_by_operation_id: Mapping[str, str] | None = None,
899+
manifest_metadata_by_operation_id: Mapping[str, Mapping[str, Any]] | None = None,
782900
) -> list[dict[str, Any]]:
783901
task_terms = _tokenize_catalog_text(task)
784902
if not task_terms:
785903
return []
786904

905+
inferred_task_intents = _infer_task_intents(task_terms)
906+
wants_dependency_context = bool(
907+
task_terms.intersection(
908+
{
909+
"build",
910+
"builder",
911+
"create",
912+
"workflow",
913+
"workflows",
914+
"agent",
915+
"agents",
916+
"workforce",
917+
"dependency",
918+
"dependencies",
919+
"configure",
920+
"design",
921+
"plan",
922+
}
923+
)
924+
)
925+
787926
ranked: list[dict[str, Any]] = []
788927
for operation in operations:
789928
operation_record = _catalog_operation_item(operation)
@@ -804,8 +943,15 @@ def _rank_catalog_operations(
804943
if max_latency_ms is not None and latency > max_latency_ms:
805944
continue
806945

946+
metadata = (
947+
manifest_metadata_by_operation_id.get(operation_record["operation_id"], {})
948+
if manifest_metadata_by_operation_id is not None
949+
else {}
950+
)
807951
support_scope = None
808-
if manifest_scope_by_operation_id is not None:
952+
if isinstance(metadata.get("support_scope"), str):
953+
support_scope = metadata["support_scope"]
954+
elif manifest_scope_by_operation_id is not None:
809955
support_scope = manifest_scope_by_operation_id.get(
810956
operation_record["operation_id"]
811957
)
@@ -815,21 +961,38 @@ def _rank_catalog_operations(
815961
elif support_scope == SUPPORT_SCOPE_BLOCKED:
816962
scope_bonus = 2.0
817963

964+
operation_intents = (
965+
_collect_manifest_intents(metadata)
966+
if isinstance(metadata, Mapping)
967+
else set()
968+
)
969+
stage = metadata.get("stage") if isinstance(metadata.get("stage"), str) else None
970+
manifest_dependencies = (
971+
_normalize_manifest_dependencies(metadata.get("dependencies"))
972+
if isinstance(metadata, Mapping)
973+
else tuple()
974+
)
975+
manifest_dependency_tokens = _tokenize_catalog_text(
976+
" ".join(manifest_dependencies)
977+
)
978+
979+
intent_bonus = _intent_bonus_for_operation(
980+
operation_intents,
981+
inferred_task_intents,
982+
)
983+
stage_bonus = _stage_bonus_for_operation(
984+
stage=stage,
985+
task_terms=task_terms,
986+
inferred_task_intents=inferred_task_intents,
987+
)
988+
818989
dependency_bonus = 0.0
819-
builder_terms = {
820-
"build",
821-
"builder",
822-
"create",
823-
"workflow",
824-
"workflows",
825-
"agent",
826-
"agents",
827-
"workforce",
828-
"dependencies",
829-
"dependency",
830-
}
831-
if task_terms.intersection(builder_terms):
832-
dependency_tokens = {
990+
if wants_dependency_context:
991+
if manifest_dependency_tokens:
992+
overlap = len(task_terms.intersection(manifest_dependency_tokens))
993+
dependency_bonus += 1.0 + min(2.0, float(overlap))
994+
995+
heuristic_dependency_tokens = {
833996
"node",
834997
"nodes",
835998
"connection",
@@ -840,16 +1003,24 @@ def _rank_catalog_operations(
8401003
"templates",
8411004
"validate",
8421005
"schema",
1006+
"tool",
1007+
"tools",
1008+
"credential",
1009+
"credentials",
1010+
"integration",
1011+
"integrations",
8431012
}
844-
dependency_bonus = float(
845-
min(3, len(operation_tokens.intersection(dependency_tokens)))
1013+
dependency_bonus += float(
1014+
min(2, len(operation_tokens.intersection(heuristic_dependency_tokens)))
8461015
)
8471016

8481017
score = round(
8491018
(relevance * 10)
8501019
- cost
8511020
- (latency / 200)
8521021
+ scope_bonus
1022+
+ intent_bonus
1023+
+ stage_bonus
8531024
+ dependency_bonus,
8541025
3,
8551026
)
@@ -861,6 +1032,11 @@ def _rank_catalog_operations(
8611032
"estimated_latency_ms": latency,
8621033
"support_scope": support_scope,
8631034
"scope_bonus": scope_bonus,
1035+
"manifest_intents": sorted(operation_intents),
1036+
"manifest_stage": stage,
1037+
"manifest_dependencies": list(manifest_dependencies),
1038+
"intent_bonus": intent_bonus,
1039+
"stage_bonus": stage_bonus,
8641040
"dependency_bonus": dependency_bonus,
8651041
"score": score,
8661042
}
@@ -1607,17 +1783,18 @@ def _run_catalog_command(
16071783
public_only=args.public_only,
16081784
spec_file=args.spec_file,
16091785
)
1610-
manifest_scope = (
1611-
_manifest_scope_by_operation_id()
1612-
if _should_use_curated_manifest(args.spec_file, args.public_only)
1613-
else None
1614-
)
1786+
manifest_scope = None
1787+
manifest_metadata = None
1788+
if _should_use_curated_manifest(args.spec_file, args.public_only):
1789+
manifest_scope = _manifest_scope_by_operation_id()
1790+
manifest_metadata = _manifest_metadata_by_operation_id()
16151791
ranked = _rank_catalog_operations(
16161792
operations,
16171793
task=args.task,
16181794
max_cost=args.max_cost,
16191795
max_latency_ms=args.max_latency_ms,
16201796
manifest_scope_by_operation_id=manifest_scope,
1797+
manifest_metadata_by_operation_id=manifest_metadata,
16211798
)
16221799
if args.json:
16231800
payload = {
@@ -1629,7 +1806,10 @@ def _run_catalog_command(
16291806
"count": len(ranked),
16301807
"heuristic": {
16311808
"name": "relevance-cost-latency",
1632-
"formula": "score = relevance*10 - cost - latency/200",
1809+
"formula": (
1810+
"score = relevance*10 - cost - latency/200 + "
1811+
"scope_bonus + intent_bonus + stage_bonus + dependency_bonus"
1812+
),
16331813
},
16341814
"items": ranked,
16351815
}
@@ -2329,11 +2509,11 @@ def _run_code_search_command(
23292509
registry: OperationRegistry,
23302510
sdk_client: AgenticFlowSDK,
23312511
) -> int:
2332-
manifest_scope = (
2333-
_manifest_scope_by_operation_id()
2334-
if _should_use_curated_manifest(args.spec_file, args.public_only)
2335-
else None
2336-
)
2512+
manifest_scope = None
2513+
manifest_metadata = None
2514+
if _should_use_curated_manifest(args.spec_file, args.public_only):
2515+
manifest_scope = _manifest_scope_by_operation_id()
2516+
manifest_metadata = _manifest_metadata_by_operation_id()
23372517
ranked = _rank_catalog_operations(
23382518
_catalog_operations(
23392519
registry,
@@ -2344,6 +2524,7 @@ def _run_code_search_command(
23442524
max_cost=args.max_cost,
23452525
max_latency_ms=args.max_latency_ms,
23462526
manifest_scope_by_operation_id=manifest_scope,
2527+
manifest_metadata_by_operation_id=manifest_metadata,
23472528
)
23482529

23492530
if args.limit is not None and args.limit > 0:

0 commit comments

Comments
 (0)