Skip to content

Commit 6b76780

Browse files
authored
Merge pull request #39 from redhat-community-ai-tools/GENIE-1076/story/display-names-instead-of-ids
GENIE-1076/story/display-names-instead-of-ids
2 parents 3626e6c + f7f8eef commit 6b76780

File tree

14 files changed

+772
-334
lines changed

14 files changed

+772
-334
lines changed

multi-agent/core/field_hints.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class ActionHint(BaseModel):
2525
...,
2626
description="Name of the action to invoke"
2727
)
28+
display_name: Optional[str] = Field(
29+
None,
30+
description="Display name for the field in the UI"
31+
)
2832
hint_type: HintType = Field(
2933
...,
3034
description="Type of hint (populate, validate)"
@@ -33,9 +37,9 @@ class ActionHint(BaseModel):
3337
None,
3438
description="Target field in action output for population hints"
3539
)
36-
label_field: Optional[str] = Field(
40+
display_field: Optional[str] = Field(
3741
None,
38-
description="Dot-notation path to display label (e.g., 'documents.name')"
42+
description="Dot-notation path to display value (e.g., 'name' or 'name.x'). UI stores full object and uses this to display."
3943
)
4044
value_field: Optional[str] = Field(
4145
None,
@@ -96,9 +100,9 @@ class ApiHint(BaseModel):
96100
None,
97101
description="Target field in response for validation hints"
98102
)
99-
label_field: Optional[str] = Field(
103+
display_field: Optional[str] = Field(
100104
None,
101-
description="Dot-notation path to display label (e.g., 'items.name')"
105+
description="Dot-notation path to display value (e.g., 'name' or 'items.name'). UI stores full object and uses this to display."
102106
)
103107
value_field: Optional[str] = Field(
104108
None,

multi-agent/elements/retrievers/docs_dataflow/config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Optional, List
1+
from typing import Dict, List, Literal, Optional
22
from pydantic import Field
33
from elements.retrievers.common.base_config import BaseRetrieverConfig
44
from core.field_hints import ActionHint, HintType, SelectionType
@@ -31,16 +31,16 @@ class DocsDataflowRetrieverConfig(BaseRetrieverConfig):
3131
description="Request timeout in seconds"
3232
)
3333

34-
doc_ids: Optional[List[str]] = Field(
34+
docs: Optional[List[Dict]] = Field(
3535
default=None,
36-
description="Filter results to specific document IDs",
36+
description="Filter results to specific documents",
3737
json_schema_extra=ActionHint(
3838
action_uid="dataflow.get_available_docs",
39+
display_name="documents",
3940
hint_type=HintType.POPULATE,
4041
selection_type=SelectionType.MANUAL,
4142
field_mapping="documents",
42-
label_field="name",
43-
value_field="id",
43+
display_field="name",
4444
multi_select=True,
4545
pagination=True,
4646
search=True,

multi-agent/elements/retrievers/docs_dataflow/docs_dataflow_retriever.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional
1+
from typing import Dict, List, Optional
22
from elements.retrievers.common.base_retriever import BaseRetriever
33
from elements.providers.dataflow_client.config import DataflowProviderConfig
44
from elements.providers.dataflow_client.dataflow_provider_factory import DataflowProviderFactory
@@ -15,11 +15,11 @@ def __init__(
1515
top_k_results: int,
1616
threshold: float,
1717
timeout: float = 30.0,
18-
doc_ids: Optional[List[str]] = None,
18+
docs: Optional[List[Dict]] = None,
1919
tags: Optional[List[str]] = None,
2020
):
2121
self.threshold = threshold
22-
self.doc_ids = doc_ids
22+
self.docs = docs
2323
self.tags = tags
2424
config = DataflowProviderConfig(
2525
top_k=top_k_results,
@@ -31,11 +31,14 @@ def __init__(
3131
def retrieve(self, query: str) -> List[dict]:
3232
context = get_current_context()
3333

34+
# Extract document IDs from docs list
35+
doc_ids = [doc['id'] for doc in self.docs] if self.docs else None
36+
3437
response = self._provider.query(
3538
query=query,
3639
scope=context.scope,
3740
logged_in_user=context.logged_in_user,
38-
doc_ids=self.doc_ids,
41+
doc_ids=doc_ids,
3942
tags=self.tags,
4043
)
4144

multi-agent/elements/retrievers/docs_dataflow/docs_dataflow_retriever_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def create(self, cfg: DocsDataflowRetrieverConfig, **kwargs: Any) -> DocsDataflo
2020
top_k_results=cfg.top_k_results,
2121
threshold=cfg.threshold,
2222
timeout=cfg.timeout,
23-
doc_ids=cfg.doc_ids,
23+
docs=cfg.docs,
2424
tags=cfg.tags,
2525
)
2626
except Exception as e:

scripts/migrate_doc_ids.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Migration Script: Convert doc_ids field from string array to object array,
4+
and rename the field from doc_ids to docs.
5+
6+
Old:
7+
doc_ids: ["id1", "id2"]
8+
9+
New:
10+
docs: [{"id": "id1", "name": "Document 1"}, ...]
11+
"""
12+
13+
import argparse
14+
import sys
15+
import json
16+
import os
17+
from datetime import datetime
18+
from typing import List, Dict
19+
import pymongo
20+
21+
22+
# ────────────────────────────────────────────────────────────────
23+
# Config
24+
# ────────────────────────────────────────────────────────────────
25+
26+
MONGODB_IP = os.environ.get("MONGODB_IP", "localhost")
27+
MONGODB_PORT = os.environ.get("MONGODB_PORT", "27017")
28+
29+
RETRIEVERS_DB = "UnifAI"
30+
SOURCES_DB = "data_sources"
31+
32+
RETRIEVERS_COLLECTION = "resources"
33+
SOURCES_COLLECTION = "sources"
34+
35+
RETRIEVER_TYPE = "docs_dataflow"
36+
CATEGORY = "retrievers"
37+
38+
39+
# ────────────────────────────────────────────────────────────────
40+
# Helpers
41+
# ────────────────────────────────────────────────────────────────
42+
43+
def needs_migration(cfg: dict) -> bool:
44+
# Needs migration if doc_ids field exists (should be renamed to docs)
45+
if "doc_ids" in cfg:
46+
return True
47+
return False
48+
49+
50+
def convert_doc_ids(old: list, docs_map: dict) -> list:
51+
out = []
52+
for d in old or []:
53+
if isinstance(d, str):
54+
out.append({"id": d, "name": docs_map.get(d, d)})
55+
elif isinstance(d, dict) and "id" in d:
56+
if "name" not in d:
57+
d["name"] = docs_map.get(d["id"], d["id"])
58+
out.append(d)
59+
return out
60+
61+
62+
# ────────────────────────────────────────────────────────────────
63+
# Migration
64+
# ────────────────────────────────────────────────────────────────
65+
66+
def migrate(dry_run: bool):
67+
client = pymongo.MongoClient(f"mongodb://{MONGODB_IP}:{MONGODB_PORT}/")
68+
69+
retrievers_db = client[RETRIEVERS_DB]
70+
sources_db = client[SOURCES_DB]
71+
72+
retrievers = retrievers_db[RETRIEVERS_COLLECTION]
73+
sources = sources_db[SOURCES_COLLECTION]
74+
75+
query = {"category": CATEGORY, "type": RETRIEVER_TYPE}
76+
resources = list(retrievers.find(query))
77+
78+
print(f"Found {len(resources)} retrievers")
79+
80+
needed_ids = set()
81+
for r in resources:
82+
if needs_migration(r.get("cfg_dict", {})):
83+
doc_ids = r.get("cfg_dict", {}).get("doc_ids") or []
84+
for d in doc_ids:
85+
if isinstance(d, str):
86+
needed_ids.add(d)
87+
88+
docs_map = {}
89+
if needed_ids:
90+
print(f"Need to resolve {len(needed_ids)} doc_ids")
91+
for src in sources.find(
92+
{"source_id": {"$in": list(needed_ids)}},
93+
{"source_id": 1, "source_name": 1}
94+
):
95+
docs_map[src["source_id"]] = src.get("source_name", src["source_id"])
96+
97+
stats = {"checked": 0, "migrated": 0, "skipped": 0}
98+
99+
for r in resources:
100+
stats["checked"] += 1
101+
rid = r.get("rid", r["_id"])
102+
name = r.get("name", "unnamed")
103+
cfg = r.get("cfg_dict", {})
104+
105+
print(f"\n[{rid}] {name}")
106+
107+
if not needs_migration(cfg):
108+
print(" Skipped")
109+
stats["skipped"] += 1
110+
continue
111+
112+
old_ids = cfg.get("doc_ids", [])
113+
new_docs = convert_doc_ids(old_ids, docs_map)
114+
115+
print(f" Old doc_ids: {old_ids}")
116+
print(f" New docs: {new_docs}")
117+
118+
if dry_run:
119+
print(" [DRY RUN] Would update (rename doc_ids -> docs)")
120+
stats["migrated"] += 1
121+
continue
122+
123+
retrievers.update_one(
124+
{"_id": r["_id"]},
125+
{
126+
"$set": {"cfg_dict.docs": new_docs, "updated": datetime.utcnow()},
127+
"$unset": {"cfg_dict.doc_ids": ""}
128+
}
129+
)
130+
131+
print(" ✓ Migrated")
132+
stats["migrated"] += 1
133+
134+
client.close()
135+
return stats
136+
137+
138+
# ────────────────────────────────────────────────────────────────
139+
# Main
140+
# ────────────────────────────────────────────────────────────────
141+
142+
def main():
143+
parser = argparse.ArgumentParser()
144+
parser.add_argument("--apply", action="store_true")
145+
args = parser.parse_args()
146+
dry_run = not args.apply
147+
148+
if dry_run:
149+
print("\n*** DRY RUN MODE ***\n")
150+
151+
stats = migrate(dry_run)
152+
153+
print("\nSummary")
154+
print(stats)
155+
156+
if dry_run:
157+
print("\nRun with --apply to commit changes.")
158+
159+
160+
if __name__ == "__main__":
161+
main()

ui/client/src/components/agentic-ai/workspace/ElementData.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
44
import { FileText } from 'lucide-react';
55
import { ElementInstance, ElementType, ElementSchema } from '../../../types/workspace';
66
import { maskSecretFieldsInConfig } from '../../../utils/maskSecretFields';
7+
import { simplifyConfigForDisplay } from '../../../utils/displayUtils';
78
import { useAgenticAI } from '@/contexts/AgenticAIContext';
89

910
interface ElementDataProps {
@@ -28,9 +29,9 @@ export const ElementData: React.FC<ElementDataProps> = ({
2829
return getResourceName(ref);
2930
});
3031

31-
// Resolve refs in config for display
32+
// Resolve refs in config for display, then simplify object arrays to just names
3233
const configWithResolvedRefs = element?.config
33-
? resolveRefsInConfig(element.config)
34+
? simplifyConfigForDisplay(resolveRefsInConfig(element.config))
3435
: null;
3536

3637
return (

ui/client/src/components/agentic-ai/workspace/ElementForm.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,22 @@ export const ElementForm: React.FC<ElementFormProps> = ({
7878
});
7979
};
8080

81-
const handlePopulateResult = (fieldName: string, results: string[] | any, multiSelect: boolean) => {
81+
const handlePopulateResult = (fieldName: string, results: any[], multiSelect: boolean) => {
8282
setPopulateResults(prev => ({
8383
...prev,
8484
[fieldName]: results
8585
}));
8686

87-
// For multi-select, set the array of selected values
88-
// For single select, set the first (and only) selected value
89-
handleInputChange(fieldName, multiSelect || typeof results === 'object' ? results : results.length > 0 ? results[0] : "");
87+
// For multi-select fields, store the full array of objects (or strings)
88+
// For single select, store just the first item (can be object or string)
89+
if (multiSelect) {
90+
// Multi-select: always store as array
91+
handleInputChange(fieldName, results);
92+
} else {
93+
// Single select: store single item (first in array, or empty string)
94+
const singleResult = results.length > 0 ? results[0] : "";
95+
handleInputChange(fieldName, singleResult);
96+
}
9097
};
9198

9299

ui/client/src/components/agentic-ai/workspace/ElementGrid.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ElementValidationResult } from '../../../types/validation';
2222
import { ElementData } from './ElementData';
2323
import { ValidationResultModal } from './ValidationResultModal';
2424
import { formatConfigValue } from '../../../utils/maskSecretFields';
25+
import { getDisplayValueFromItem } from '../../../utils/displayUtils';
2526

2627
interface ElementGridProps {
2728
elements: ElementInstance[];
@@ -224,8 +225,8 @@ export const ElementGrid: React.FC<ElementGridProps> = ({
224225
const fieldSchema = elementSchema?.config_schema?.properties?.[key];
225226
const rawValue = element.config[key];
226227
const displayValue = Array.isArray(rawValue)
227-
? rawValue.map((item: any) => getResourceName(item)).join(', ')
228-
: getResourceName(rawValue);
228+
? rawValue.map((item: any) => getDisplayValueFromItem(item, getResourceName)).join(', ')
229+
: getDisplayValueFromItem(rawValue, getResourceName);
229230
return (
230231
<div key={key} className="flex justify-between">
231232
<span className="truncate">{key}:</span>

0 commit comments

Comments
 (0)