Loading src/agentic/README.md +14 −0 Changes for src/agentic/README.md: 14 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -335,6 +335,20 @@ create a 50 GHz optical service from DC1-TP1 to DC2-TP1 A successful mutating workflow must report the created TFS service UUIDs and verify that every segment reaches `SERVICESTATUS_ACTIVE`. ### Optical Spectrum Questions Detailed service retrieval includes a deterministic `spectrum_comparison` decoded from controller `/settings` rules. It compares allocations within the same domain, directed optical link and band; reverse directions and different bands are independent. Missing allocation data marks the comparison incomplete rather than proving that no conflicts exist. This comparison checks reported service allocations, not link occupancy or other reservations. Detailed optical-link retrieval with `include_spectrum` also provides compact free/unavailable ranges alongside the original maps: `1` means free and `0` means unavailable. Unavailable slots can include blocked or unsupported spectrum, not only allocations to active services. ## Tests Unit tests validate configuration parsing, peer parsing, and deterministic Loading src/agentic/service/agents/retrieval.py +10 −2 Changes for src/agentic/service/agents/retrieval.py: 10 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -132,7 +132,12 @@ You execute local service retrieval. Use query_local_service_headers for normal list/count/summary questions. Use query_local_detailed_services only for raw, full, UUID, config-rule, or debug requests. Do not confuse services with links or connections. debug requests, and for spectrum allocations or overlap checks. Use spectrum_comparison; report directed link, band and slots together. Opposite directions and different bands are independent resources. An incomplete comparison is not proof of no conflict. Service allocations alone do not verify optical-link occupancy or other reservations. Do not confuse services with links or connections. """ CROSS_DOMAIN_SERVICE_RETRIEVAL_INSTRUCTION = """ Loading @@ -140,7 +145,10 @@ You execute cross-domain service retrieval. Use query_cross_domain_service_headers for normal list/count/summary questions. Use query_cross_domain_detailed_services only for raw, full, UUID, config-rule, or debug requests. Do not confuse services with links or connections. or debug requests, and for spectrum allocations or overlap checks. Use spectrum_comparison and compare only within the same domain, directed link and band. Report all three dimensions and any incomplete checks. Do not confuse services with links or connections. """ LOCAL_CONNECTION_RETRIEVAL_INSTRUCTION = """ Loading src/agentic/service/agents/single.py +16 −0 Changes for src/agentic/service/agents/single.py: 16 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -76,6 +76,22 @@ For read-only requests: identifiers, configuration rules, or full payloads. - Endpoint ports, service paths and connection details also require detailed retrieval. Resolve UUIDs against inventory; do not infer paths from names. - For service spectrum questions use detailed services and its deterministic spectrum_comparison. Report direction AND band beside every slot range. R1-R2 and R2-R1 are different resources; never merge their allocations. Only compare slots on the same domain, directed link and band. If the check is incomplete, say so. Do not claim link occupancy was verified from service records alone; retrieve optical-link spectrum for an occupancy question. - List every relevant service with its exact directed link, band and slots, including the reverse direction. Do not say all slot numbers are distinct when numbers repeat on independent resources; say no same-resource overlap. For overlap checks use a table with Service, Directed link, Band, Slots; include singleton groups too, not just groups containing several services. Use spectrum_comparison.allocation_table as the basis for that table; preserve all matching rows, including single reverse-direction services. - Optical spectrum maps use 1=free and 0=unavailable. Use spectrum_summary for ranges/counts. Zero free slots does NOT mean zero occupancy. Unavailable slots may be blocked or unsupported; do not attribute all zeros to services. - Set include_config_rules or include_spectrum only when the user asks for configuration rules or spectrum maps, respectively. - An empty packet-link inventory does not imply there are no optical links. Loading src/agentic/service/tools/granular.py +22 −2 Changes for src/agentic/service/tools/granular.py: 22 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -21,6 +21,11 @@ import uuid from google.adk.tools import FunctionTool from agentic.service.tools.service_spectrum import ( summarize_link_spectrum, summarize_service_spectrum, ) from agentic.service.tools.service import ( delete_cross_domain_optical_service, delete_service, Loading Loading @@ -252,7 +257,20 @@ async def query_detailed_optical_links( detail_level="full", include_ids=True, ) if not include_spectrum: if include_spectrum: inventory = copy.deepcopy(inventory) inventory["spectrum_interpretation"] = ( "1 = free, 0 = unavailable. Use spectrum_summary ranges, not " "visual inspection of raw maps. Unavailable includes blocked " "or unsupported slots, not only service allocations. An empty " "map means no slot data, not zero occupancy." ) for domain in inventory.get("domains", {}).values(): for link in domain.get("optical_links", []): link["spectrum_summary"] = summarize_link_spectrum( link.get("optical_details", {}) ) else: inventory = copy.deepcopy(inventory) for domain in inventory.get("domains", {}).values(): for link in domain.get("optical_links", []): Loading @@ -277,13 +295,15 @@ async def query_services(scope: str = "all") -> dict: async def query_detailed_services(scope: str = "all") -> dict: """Query detailed services in local, remote, or all domains.""" return await _list_domain_inventory( inventory = await _list_domain_inventory( scope=scope, resource_kind="services", detail_level="full", include_config_rules=True, include_ids=True, ) inventory["spectrum_comparison"] = summarize_service_spectrum(inventory) return inventory async def query_connections(scope: str = "all") -> dict: Loading src/agentic/service/tools/service_spectrum.py 0 → 100644 +154 −0 Changes for src/agentic/service/tools/service_spectrum.py: 154 added lines, 0 removed lines. Original line number Diff line number Diff line # Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Compare reported service allocations, not physical spectrum availability.""" import json from itertools import combinations BANDS = { "C_BAND": "c_slots", "L_BAND": "l_slots", "S_BAND": "s_slots", "c_slots": "c_slots", "l_slots": "l_slots", "s_slots": "s_slots", } def slot_ranges(slots): ranges = [] for slot in sorted(set(slots)): if ranges and slot == ranges[-1][1] + 1: ranges[-1][1] = slot else: ranges.append([slot, slot]) return ranges def summarize_link_spectrum(details): result = {} for band in ("c_slots", "l_slots", "s_slots"): bitmap = details.get(band, {}) free = [int(key) for key, value in bitmap.items() if value == 1] unavailable = [int(key) for key, value in bitmap.items() if value == 0] result[band] = { "free_ranges": slot_ranges(free), "unavailable_ranges": slot_ranges(unavailable), "free_count": len(free), "unavailable_count": len(unavailable), "reported_count": len(bitmap), } return result def summarize_service_spectrum(inventory): allocations = [] incomplete = [] for domain_id, domain in inventory.get("domains", {}).items(): if domain.get("error"): incomplete.append({"domain": domain_id, "reason": "query_failed"}) for service in domain.get("services", []): service_type = service.get("service_type") if service_type not in (6, "6", "SERVICETYPE_OPTICAL_CONNECTIVITY"): continue identity = { "domain": domain_id, "service_name": service.get("name", ""), "service_id": service.get("service_id", {}), } rules = service.get("service_config", {}).get("config_rules", []) settings = None try: values = [ rule["custom"]["resource_value"] for rule in rules if rule.get("custom", {}).get("resource_key") == "/settings" ] if len(values) != 1: raise ValueError("missing or ambiguous /settings") settings = json.loads(values[0]) band = BANDS.get(settings.get("band_type")) links = settings.get("links") slots = settings.get("slots") if not band or not isinstance(links, list) or not links: raise ValueError("missing band or directed links") if not all(isinstance(link, str) and link for link in links): raise ValueError("invalid directed links") if not isinstance(slots, list) or not slots: raise ValueError("missing slots") if not all(type(slot) is int and slot >= 0 for slot in slots): raise ValueError("invalid slots") except (ValueError, TypeError, AttributeError, KeyError) as error: incomplete.append({**identity, "reason": str(error)}) continue allocations.append({ **identity, "status": service.get("status", service.get("service_status")), "band": band, "directed_links": links, "path": settings.get("path", []), "slots": sorted(set(slots)), }) groups = {} for allocation in allocations: for link in set(allocation["directed_links"]): key = (allocation["domain"], link, allocation["band"]) groups.setdefault(key, []).append(allocation) comparisons = [] for (domain, link, band), members in sorted(groups.items()): overlaps = [] for first, second in combinations(members, 2): overlap = sorted(set(first["slots"]) & set(second["slots"])) if overlap: overlaps.append({ "services": [ first["service_name"], second["service_name"], ], "slots": overlap, }) comparisons.append({ "domain": domain, "directed_link": link, "band": band, "allocations": [ {"service_name": member["service_name"], "slots": member["slots"]} for member in members ], "overlaps": overlaps, }) rows = ["Domain | Service | Directed links | Band | Slots", "--- | --- | --- | --- | ---"] for allocation in allocations: ranges = ", ".join( str(start) if start == end else f"{start}-{end}" for start, end in slot_ranges(allocation["slots"]) ) rows.append(" | ".join([ str(allocation["domain"]), allocation["service_name"], ", ".join(allocation["directed_links"]), allocation["band"], ranges, ])) return { "basis": "Retrieved service /settings allocations only", "allocation_table": "\n".join(rows), "comparison_key": ["domain", "directed_link", "band"], "allocations": allocations, "link_comparisons": comparisons, "overlap_count": sum(len(row["overlaps"]) for row in comparisons), "complete": not incomplete and inventory.get("ok", False), "incomplete": incomplete, "occupancy_verified": False, "interpretation": ( "Report exact directed links and bands. Reversed links and " "different bands are independent resources. No reported overlap " "does not verify optical-link occupancy or absence of other " "reservations. Missing allocation data makes the check incomplete." ), } Loading
src/agentic/README.md +14 −0 Changes for src/agentic/README.md: 14 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -335,6 +335,20 @@ create a 50 GHz optical service from DC1-TP1 to DC2-TP1 A successful mutating workflow must report the created TFS service UUIDs and verify that every segment reaches `SERVICESTATUS_ACTIVE`. ### Optical Spectrum Questions Detailed service retrieval includes a deterministic `spectrum_comparison` decoded from controller `/settings` rules. It compares allocations within the same domain, directed optical link and band; reverse directions and different bands are independent. Missing allocation data marks the comparison incomplete rather than proving that no conflicts exist. This comparison checks reported service allocations, not link occupancy or other reservations. Detailed optical-link retrieval with `include_spectrum` also provides compact free/unavailable ranges alongside the original maps: `1` means free and `0` means unavailable. Unavailable slots can include blocked or unsupported spectrum, not only allocations to active services. ## Tests Unit tests validate configuration parsing, peer parsing, and deterministic Loading
src/agentic/service/agents/retrieval.py +10 −2 Changes for src/agentic/service/agents/retrieval.py: 10 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -132,7 +132,12 @@ You execute local service retrieval. Use query_local_service_headers for normal list/count/summary questions. Use query_local_detailed_services only for raw, full, UUID, config-rule, or debug requests. Do not confuse services with links or connections. debug requests, and for spectrum allocations or overlap checks. Use spectrum_comparison; report directed link, band and slots together. Opposite directions and different bands are independent resources. An incomplete comparison is not proof of no conflict. Service allocations alone do not verify optical-link occupancy or other reservations. Do not confuse services with links or connections. """ CROSS_DOMAIN_SERVICE_RETRIEVAL_INSTRUCTION = """ Loading @@ -140,7 +145,10 @@ You execute cross-domain service retrieval. Use query_cross_domain_service_headers for normal list/count/summary questions. Use query_cross_domain_detailed_services only for raw, full, UUID, config-rule, or debug requests. Do not confuse services with links or connections. or debug requests, and for spectrum allocations or overlap checks. Use spectrum_comparison and compare only within the same domain, directed link and band. Report all three dimensions and any incomplete checks. Do not confuse services with links or connections. """ LOCAL_CONNECTION_RETRIEVAL_INSTRUCTION = """ Loading
src/agentic/service/agents/single.py +16 −0 Changes for src/agentic/service/agents/single.py: 16 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -76,6 +76,22 @@ For read-only requests: identifiers, configuration rules, or full payloads. - Endpoint ports, service paths and connection details also require detailed retrieval. Resolve UUIDs against inventory; do not infer paths from names. - For service spectrum questions use detailed services and its deterministic spectrum_comparison. Report direction AND band beside every slot range. R1-R2 and R2-R1 are different resources; never merge their allocations. Only compare slots on the same domain, directed link and band. If the check is incomplete, say so. Do not claim link occupancy was verified from service records alone; retrieve optical-link spectrum for an occupancy question. - List every relevant service with its exact directed link, band and slots, including the reverse direction. Do not say all slot numbers are distinct when numbers repeat on independent resources; say no same-resource overlap. For overlap checks use a table with Service, Directed link, Band, Slots; include singleton groups too, not just groups containing several services. Use spectrum_comparison.allocation_table as the basis for that table; preserve all matching rows, including single reverse-direction services. - Optical spectrum maps use 1=free and 0=unavailable. Use spectrum_summary for ranges/counts. Zero free slots does NOT mean zero occupancy. Unavailable slots may be blocked or unsupported; do not attribute all zeros to services. - Set include_config_rules or include_spectrum only when the user asks for configuration rules or spectrum maps, respectively. - An empty packet-link inventory does not imply there are no optical links. Loading
src/agentic/service/tools/granular.py +22 −2 Changes for src/agentic/service/tools/granular.py: 22 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -21,6 +21,11 @@ import uuid from google.adk.tools import FunctionTool from agentic.service.tools.service_spectrum import ( summarize_link_spectrum, summarize_service_spectrum, ) from agentic.service.tools.service import ( delete_cross_domain_optical_service, delete_service, Loading Loading @@ -252,7 +257,20 @@ async def query_detailed_optical_links( detail_level="full", include_ids=True, ) if not include_spectrum: if include_spectrum: inventory = copy.deepcopy(inventory) inventory["spectrum_interpretation"] = ( "1 = free, 0 = unavailable. Use spectrum_summary ranges, not " "visual inspection of raw maps. Unavailable includes blocked " "or unsupported slots, not only service allocations. An empty " "map means no slot data, not zero occupancy." ) for domain in inventory.get("domains", {}).values(): for link in domain.get("optical_links", []): link["spectrum_summary"] = summarize_link_spectrum( link.get("optical_details", {}) ) else: inventory = copy.deepcopy(inventory) for domain in inventory.get("domains", {}).values(): for link in domain.get("optical_links", []): Loading @@ -277,13 +295,15 @@ async def query_services(scope: str = "all") -> dict: async def query_detailed_services(scope: str = "all") -> dict: """Query detailed services in local, remote, or all domains.""" return await _list_domain_inventory( inventory = await _list_domain_inventory( scope=scope, resource_kind="services", detail_level="full", include_config_rules=True, include_ids=True, ) inventory["spectrum_comparison"] = summarize_service_spectrum(inventory) return inventory async def query_connections(scope: str = "all") -> dict: Loading
src/agentic/service/tools/service_spectrum.py 0 → 100644 +154 −0 Changes for src/agentic/service/tools/service_spectrum.py: 154 added lines, 0 removed lines. Original line number Diff line number Diff line # Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Compare reported service allocations, not physical spectrum availability.""" import json from itertools import combinations BANDS = { "C_BAND": "c_slots", "L_BAND": "l_slots", "S_BAND": "s_slots", "c_slots": "c_slots", "l_slots": "l_slots", "s_slots": "s_slots", } def slot_ranges(slots): ranges = [] for slot in sorted(set(slots)): if ranges and slot == ranges[-1][1] + 1: ranges[-1][1] = slot else: ranges.append([slot, slot]) return ranges def summarize_link_spectrum(details): result = {} for band in ("c_slots", "l_slots", "s_slots"): bitmap = details.get(band, {}) free = [int(key) for key, value in bitmap.items() if value == 1] unavailable = [int(key) for key, value in bitmap.items() if value == 0] result[band] = { "free_ranges": slot_ranges(free), "unavailable_ranges": slot_ranges(unavailable), "free_count": len(free), "unavailable_count": len(unavailable), "reported_count": len(bitmap), } return result def summarize_service_spectrum(inventory): allocations = [] incomplete = [] for domain_id, domain in inventory.get("domains", {}).items(): if domain.get("error"): incomplete.append({"domain": domain_id, "reason": "query_failed"}) for service in domain.get("services", []): service_type = service.get("service_type") if service_type not in (6, "6", "SERVICETYPE_OPTICAL_CONNECTIVITY"): continue identity = { "domain": domain_id, "service_name": service.get("name", ""), "service_id": service.get("service_id", {}), } rules = service.get("service_config", {}).get("config_rules", []) settings = None try: values = [ rule["custom"]["resource_value"] for rule in rules if rule.get("custom", {}).get("resource_key") == "/settings" ] if len(values) != 1: raise ValueError("missing or ambiguous /settings") settings = json.loads(values[0]) band = BANDS.get(settings.get("band_type")) links = settings.get("links") slots = settings.get("slots") if not band or not isinstance(links, list) or not links: raise ValueError("missing band or directed links") if not all(isinstance(link, str) and link for link in links): raise ValueError("invalid directed links") if not isinstance(slots, list) or not slots: raise ValueError("missing slots") if not all(type(slot) is int and slot >= 0 for slot in slots): raise ValueError("invalid slots") except (ValueError, TypeError, AttributeError, KeyError) as error: incomplete.append({**identity, "reason": str(error)}) continue allocations.append({ **identity, "status": service.get("status", service.get("service_status")), "band": band, "directed_links": links, "path": settings.get("path", []), "slots": sorted(set(slots)), }) groups = {} for allocation in allocations: for link in set(allocation["directed_links"]): key = (allocation["domain"], link, allocation["band"]) groups.setdefault(key, []).append(allocation) comparisons = [] for (domain, link, band), members in sorted(groups.items()): overlaps = [] for first, second in combinations(members, 2): overlap = sorted(set(first["slots"]) & set(second["slots"])) if overlap: overlaps.append({ "services": [ first["service_name"], second["service_name"], ], "slots": overlap, }) comparisons.append({ "domain": domain, "directed_link": link, "band": band, "allocations": [ {"service_name": member["service_name"], "slots": member["slots"]} for member in members ], "overlaps": overlaps, }) rows = ["Domain | Service | Directed links | Band | Slots", "--- | --- | --- | --- | ---"] for allocation in allocations: ranges = ", ".join( str(start) if start == end else f"{start}-{end}" for start, end in slot_ranges(allocation["slots"]) ) rows.append(" | ".join([ str(allocation["domain"]), allocation["service_name"], ", ".join(allocation["directed_links"]), allocation["band"], ranges, ])) return { "basis": "Retrieved service /settings allocations only", "allocation_table": "\n".join(rows), "comparison_key": ["domain", "directed_link", "band"], "allocations": allocations, "link_comparisons": comparisons, "overlap_count": sum(len(row["overlaps"]) for row in comparisons), "complete": not incomplete and inventory.get("ok", False), "incomplete": incomplete, "occupancy_verified": False, "interpretation": ( "Report exact directed links and bands. Reversed links and " "different bands are independent resources. No reported overlap " "does not verify optical-link occupancy or absence of other " "reservations. Missing allocation data makes the check incomplete." ), }