prompts + docs: did_block_overlap, partition_summary, schema landmarks
Closes items 4-7 of cucx-docs's prompt-suggestions roadmap (see axl/agent-threads/cucx-prompt-suggestions/ for the source thread). did_block_overlap(block_pattern) — new prompt. LLM-orchestrated audit that finds carveout patterns inside a DID block and surfaces silent routing exceptions (e.g., 9498/9499 carved out of the 20878594XX block to route to a different fax server). Composes the existing route_patterns(filter=) tool with post-processing rather than introducing a new tool — cucx-docs's #3 was originally pitched as a tool, but the audit-narrative output is more naturally a prompt. partition_summary(partition_name=None) — new prompt. "What is this partition for?" orientation report composing route_partitions, route_patterns, route_calling_search_spaces, and the new route_patterns_targeting. No new SQL — this is pure orchestration. Useful when walking into an unfamiliar cluster and seeing a partition name like RTC-MGW-Inbound and needing to figure out its role before touching anything. cucm_sql_help — deepened with five schema-landmark sections that cost real audit sessions 3-5 query attempts each to discover. Topics: numplan↔device M:N via devicenumplanmap; non-existence of sipdestination as a table; routelist (singular) ≠ numplan→RL; LEFT-JOIN convention for type-decoder enum tables; CDR/CMR timestamp localization (cluster-TZ-conditional). Also updated the docs-search reference from "cisco-docs MCP" to "mcdewey MCP" to match yesterday's rename. cucm-schema-cheatsheet docs — appended a "Schema gotchas (from real audit sessions)" section mirroring the cucm_sql_help content. Two locations because they serve different consumers: the prompt is read by an LLM at query time, the docs page is read by a human reviewing the cluster offline. Tests: registration sentinel updated to include the two new prompts (catches the case where a new module is added without a server.py shim — the prompt would otherwise be invisible to the LLM). Full suite still 238 passing. Q3 verification (CDR timestamp empirical) still pending — cluster TLS intermittent this session. The schema-landmark text is conditional on cluster TZ per cucx-docs's caveat, so even an unverified ship is defensible.
This commit is contained in:
parent
9427e3d4df
commit
a07f8c7291
@ -187,8 +187,92 @@ Examples: `sipdevice.isanonymous`, `sipdevice.acceptinboundrdnis`,
|
|||||||
bucket have `fkroutepartition = NULL`. The default-CSS rules apply
|
bucket have `fkroutepartition = NULL`. The default-CSS rules apply
|
||||||
to them.
|
to them.
|
||||||
|
|
||||||
|
## Schema gotchas (from real audit sessions)
|
||||||
|
|
||||||
|
Each landmark below cost a multi-attempt schema-discovery walk in
|
||||||
|
actual CUCM 15 audits. They're listed here so the next operator skips
|
||||||
|
that walk. `axl_sql` also appends a corrective hint at the error layer
|
||||||
|
for the first two — the docs version is for when you're composing a
|
||||||
|
query *before* it errors.
|
||||||
|
|
||||||
|
### `numplan` ↔ `device` is M:N, not a direct foreign key
|
||||||
|
|
||||||
|
`numplan` does **not** have an `fkdevice` column. The link goes through
|
||||||
|
`devicenumplanmap`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
JOIN devicenumplanmap m ON m.fknumplan = numplan.pkid
|
||||||
|
JOIN device d ON m.fkdevice = d.pkid
|
||||||
|
```
|
||||||
|
|
||||||
|
A natural-sounding `WHERE numplan.fkdevice = ?` errors with
|
||||||
|
`Column (fkdevice) not found`. `axl_sql` appends a hint pointing at
|
||||||
|
the join table; this docs entry is the same fact in the pre-write
|
||||||
|
phase. The `route_patterns_targeting(device_name=...)` tool wraps
|
||||||
|
this join shape if you just want the inverse query.
|
||||||
|
|
||||||
|
### `sipdestination` is not a table
|
||||||
|
|
||||||
|
Reasonable-sounding name; doesn't exist as an Informix table. SIP
|
||||||
|
trunk destinations live on `device` joined with `sipdestinationgroup`
|
||||||
|
and `sipprofile`. Run:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- See actual SIP-related tables on this cluster:
|
||||||
|
axl_list_tables(pattern='sip%')
|
||||||
|
```
|
||||||
|
|
||||||
|
`axl_sql` appends a hint when a query references `sipdestination`
|
||||||
|
and hits a "table not in database" error.
|
||||||
|
|
||||||
|
### `routelist` (singular) is the RL→RG join, not numplan→RL
|
||||||
|
|
||||||
|
The naming is confusable. `routelist` holds the
|
||||||
|
`(fkdevice, fkroutegroup, selectionorder)` rows that compose a Route
|
||||||
|
List from its Route Groups. It is **not** the link from a route
|
||||||
|
pattern (`numplan`) to a route list — that link is
|
||||||
|
`numplan.fkdestination`, which can point at a Route List's device
|
||||||
|
pkid, an external phone, etc.
|
||||||
|
|
||||||
|
### Always `LEFT JOIN` the enum-decoder type tables
|
||||||
|
|
||||||
|
`typeclass`, `typemodel`, `typepatternusage`, `typecountry`,
|
||||||
|
`typecallingsearchspaceuse`, etc. hold integer enum → name mappings.
|
||||||
|
**Always `LEFT JOIN`, not `INNER JOIN`** — if Cisco adds a new enum
|
||||||
|
value in a future release, an inner join silently drops rows for
|
||||||
|
that new value, while a left join returns the row with NULL in the
|
||||||
|
enum-name column. NULL is something an auditor notices and
|
||||||
|
investigates; a missing row is a silent gap.
|
||||||
|
|
||||||
|
### CDR/CMR timestamps are local-time-as-UTC-seconds (cluster-TZ-conditional)
|
||||||
|
|
||||||
|
When the cluster's TZ is anything other than UTC, the CDR Repository
|
||||||
|
writes wall-clock-as-epoch into `dateTimeOrigination`,
|
||||||
|
`dateTimeConnect`, `dateTimeDisconnect`, etc. Decoding via
|
||||||
|
`TO_TIMESTAMP()` returns the correct *value* (e.g. `06:53:31`) but
|
||||||
|
mislabels it as UTC. **Don't double-convert with `AT TIME ZONE`** —
|
||||||
|
the timestamps are already in local time.
|
||||||
|
|
||||||
|
This is conditional on the cluster's TZ setting. A UTC-configured
|
||||||
|
cluster emits genuine UTC. Verify before assuming:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 5 ds.name, tz.name AS timezone
|
||||||
|
FROM datetimesetting ds
|
||||||
|
LEFT JOIN typetimezone tz ON tz.enum = ds.tktimezone
|
||||||
|
```
|
||||||
|
|
||||||
|
If the result shows anything non-UTC (e.g. `America/Denver`), CDR
|
||||||
|
timestamps from that cluster are local-as-epoch and tooling that
|
||||||
|
treats them as UTC will display correct values with a wrong label —
|
||||||
|
or, worse, double-convert and produce wrong values.
|
||||||
|
|
||||||
|
[`mcsiphon`](https://pypi.org/project/mcsiphon/) (operational
|
||||||
|
diagnostics MCP server) suffixes its CDR timestamp fields with
|
||||||
|
`_local` to make the convention visible to downstream tooling.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [Tools reference](/reference/tools/) — the helpers that abstract over the gotchas above
|
- [Tools reference](/reference/tools/) — the helpers that abstract over the gotchas above
|
||||||
- [SIP trunk report](/how-to/sip-trunk-report/) — worked example with all the joins
|
- [SIP trunk report](/how-to/sip-trunk-report/) — worked example with all the joins
|
||||||
- Cisco's official CUCM data dictionary — search via `cisco-docs` MCP for "data dictionary"
|
- Cisco's official CUCM data dictionary — search via [`mcdewey`](https://pypi.org/project/mcdewey/) MCP for "data dictionary"
|
||||||
|
|||||||
@ -20,9 +20,11 @@ shim is where the parameter contract lives.
|
|||||||
from . import (
|
from . import (
|
||||||
audit_routing,
|
audit_routing,
|
||||||
cucm_sql_help,
|
cucm_sql_help,
|
||||||
|
did_block_overlap,
|
||||||
hunt_pilot_audit,
|
hunt_pilot_audit,
|
||||||
inbound_did_audit,
|
inbound_did_audit,
|
||||||
investigate_pattern,
|
investigate_pattern,
|
||||||
|
partition_summary,
|
||||||
phone_inventory_report,
|
phone_inventory_report,
|
||||||
route_plan_overview,
|
route_plan_overview,
|
||||||
sip_trunk_report,
|
sip_trunk_report,
|
||||||
@ -33,9 +35,11 @@ from . import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"audit_routing",
|
"audit_routing",
|
||||||
"cucm_sql_help",
|
"cucm_sql_help",
|
||||||
|
"did_block_overlap",
|
||||||
"hunt_pilot_audit",
|
"hunt_pilot_audit",
|
||||||
"inbound_did_audit",
|
"inbound_did_audit",
|
||||||
"investigate_pattern",
|
"investigate_pattern",
|
||||||
|
"partition_summary",
|
||||||
"phone_inventory_report",
|
"phone_inventory_report",
|
||||||
"route_plan_overview",
|
"route_plan_overview",
|
||||||
"sip_trunk_report",
|
"sip_trunk_report",
|
||||||
|
|||||||
@ -38,12 +38,74 @@ The user asks: **{question}**
|
|||||||
2. Run `axl_describe_table(<table_name>)` for the candidate tables to see
|
2. Run `axl_describe_table(<table_name>)` for the candidate tables to see
|
||||||
exact column names and types.
|
exact column names and types.
|
||||||
3. If the schema chunks below already answer the question, draft the SQL
|
3. If the schema chunks below already answer the question, draft the SQL
|
||||||
directly. If not, also invoke the `cisco-docs` MCP server's `search_docs`
|
directly. If not, also invoke the `mcdewey` MCP server's `search_docs`
|
||||||
tool with a relevant query (e.g., search_docs("data dictionary table for X")).
|
tool with a relevant query (e.g., search_docs("data dictionary table for X")).
|
||||||
4. Compose the SELECT, run it via `axl_sql(query=...)`.
|
4. Compose the SELECT, run it via `axl_sql(query=...)`.
|
||||||
5. Summarize the result for the user — counts, anomalies, and what you'd
|
5. Summarize the result for the user — counts, anomalies, and what you'd
|
||||||
recommend doing about them.
|
recommend doing about them.
|
||||||
|
|
||||||
|
## Schema landmarks worth knowing before you start
|
||||||
|
|
||||||
|
A handful of CUCM Informix schema details that cost real audit sessions
|
||||||
|
3-5 query attempts each before the right path was found. If your
|
||||||
|
question touches any of these areas, jump straight to the right shape
|
||||||
|
instead of rediscovering it.
|
||||||
|
|
||||||
|
### `numplan` ↔ `device` is **M:N**, not a direct foreign key
|
||||||
|
|
||||||
|
`numplan` does **not** have an `fkdevice` column. The link goes through
|
||||||
|
`devicenumplanmap`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
JOIN devicenumplanmap m ON m.fknumplan = numplan.pkid
|
||||||
|
JOIN device d ON m.fkdevice = d.pkid
|
||||||
|
```
|
||||||
|
|
||||||
|
A natural-sounding `WHERE numplan.fkdevice = ?` will fail with
|
||||||
|
"Column (fkdevice) not found" — and `axl_sql` now appends a corrective
|
||||||
|
hint to that error, but you'll save the round-trip if you start with
|
||||||
|
the join above.
|
||||||
|
|
||||||
|
### `routelist` (singular) is the RL→RG join, not numplan→RL
|
||||||
|
|
||||||
|
The naming is confusable. `routelist` holds the `(fkdevice, fkroutegroup,
|
||||||
|
selectionorder)` rows that compose a Route List from its Route Groups.
|
||||||
|
It is **not** the link from a route pattern (`numplan`) to a route list.
|
||||||
|
That link is `numplan.fkdestination` (which can point at a Route List's
|
||||||
|
device pkid, an external phone, etc.).
|
||||||
|
|
||||||
|
### `sipdestination` does not exist
|
||||||
|
|
||||||
|
Reasonable-sounding name, surfaces in some Cisco docs, but it isn't an
|
||||||
|
Informix table. SIP trunk destinations live on `device` plus the
|
||||||
|
`sipdestinationgroup` and `sipprofile` join tables. Run
|
||||||
|
`axl_list_tables(pattern='sip%')` to see the actual tables, and
|
||||||
|
`axl_sql` will append a corrective hint if you try to FROM it.
|
||||||
|
|
||||||
|
### Always `LEFT JOIN` the type-decoder enum tables
|
||||||
|
|
||||||
|
`typeclass`, `typemodel`, `typepatternusage`, `typecountry`, `typecallingsearchspaceuse`, etc.
|
||||||
|
hold integer enum → name mappings. **Always use `LEFT JOIN`, not `INNER
|
||||||
|
JOIN`** — if Cisco adds a new enum value in a future release, an inner
|
||||||
|
join silently drops the row, while a left join still returns the row
|
||||||
|
with NULL in the enum-name column. The latter is something an auditor
|
||||||
|
sees and acts on; the former is a silent gap.
|
||||||
|
|
||||||
|
### CDR/CMR timestamps are stored as local-time-as-UTC-seconds (not actual UTC)
|
||||||
|
|
||||||
|
When the cluster's TZ is set to anything other than UTC, the CDR
|
||||||
|
Repository service writes wall-clock-as-epoch into `dateTimeOrigination`,
|
||||||
|
`dateTimeConnect`, `dateTimeDisconnect`, etc. So decoding via
|
||||||
|
`TO_TIMESTAMP()` returns the correct *value* (e.g. `06:53:31`) but
|
||||||
|
mislabels it as UTC. Don't double-convert with `AT TIME ZONE` — the
|
||||||
|
timestamps are already in local time.
|
||||||
|
|
||||||
|
A UTC-configured cluster (rare in production) emits genuine UTC
|
||||||
|
timestamps; this is conditional on the cluster's TZ setting, not a
|
||||||
|
universal truth. If your question involves CDR analysis or time windows,
|
||||||
|
verify the cluster TZ first via `axl_describe_table('datetimesetting')`
|
||||||
|
+ a query, or treat all CDR times as local with `_local` suffixes.
|
||||||
|
|
||||||
## Possibly relevant schema chunks
|
## Possibly relevant schema chunks
|
||||||
|
|
||||||
{schema_block}
|
{schema_block}
|
||||||
|
|||||||
136
src/mcaxl/prompts/did_block_overlap.py
Normal file
136
src/mcaxl/prompts/did_block_overlap.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
"""DID block overlap audit — find carveout patterns inside larger blocks.
|
||||||
|
|
||||||
|
Surfaces the kind of finding that's easy to miss visually but material
|
||||||
|
for any DID-block audit: a more-specific pattern carved out of a larger
|
||||||
|
block (e.g., `9498` and `9499` carved out of the RightFax `20878594XX`
|
||||||
|
100-DID block, routing those 2 DIDs to ZetaFax instead).
|
||||||
|
|
||||||
|
Per CUCM's longest-match rule, the more-specific patterns win. Knowing
|
||||||
|
where carveouts exist tells the operator about routing exceptions that
|
||||||
|
documentation often omits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._common import render_schema_block
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..docs_loader import DocsIndex
|
||||||
|
|
||||||
|
|
||||||
|
_KEYWORDS = [
|
||||||
|
"route pattern", "longest match", "translation pattern",
|
||||||
|
"wildcard", "DID block", "called party",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render(docs: "DocsIndex | None", block_pattern: str) -> str:
|
||||||
|
"""Audit a DID block for carveout patterns that route differently.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block_pattern: the nominal block, e.g. `"20878594XX"`,
|
||||||
|
`"+1208524XXXX"`. Wildcard syntax is CUCM's (X for any digit,
|
||||||
|
[a-b] for ranges, sets, etc.).
|
||||||
|
"""
|
||||||
|
schema_block = render_schema_block(
|
||||||
|
docs, _KEYWORDS, max_chunks=3, max_chars_per_chunk=800
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""# DID Block Overlap Audit — `{block_pattern}`
|
||||||
|
|
||||||
|
Find every numplan entry that matches *anywhere* within the nominal
|
||||||
|
block `{block_pattern}`, grouped by destination device, with carveouts
|
||||||
|
flagged. Per CUCM's longest-match rule, more-specific patterns win —
|
||||||
|
which means a carveout silently overrides the block for those digits.
|
||||||
|
|
||||||
|
## Step 1 — Discover all numplan entries that fall in or near the block
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
np.dnorpattern AS pattern,
|
||||||
|
rp.name AS partition,
|
||||||
|
np.description,
|
||||||
|
tpu.name AS pattern_type,
|
||||||
|
d.name AS destination_device,
|
||||||
|
tc.name AS destination_class
|
||||||
|
FROM numplan np
|
||||||
|
LEFT OUTER JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
LEFT OUTER JOIN typepatternusage tpu ON np.tkpatternusage = tpu.enum
|
||||||
|
LEFT OUTER JOIN devicenumplanmap m ON m.fknumplan = np.pkid
|
||||||
|
LEFT OUTER JOIN device d ON d.pkid = m.fkdevice
|
||||||
|
LEFT OUTER JOIN typeclass tc ON tc.enum = d.tkclass
|
||||||
|
WHERE np.dnorpattern LIKE '<prefix>%'
|
||||||
|
ORDER BY np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<prefix>` with the literal-prefix portion of `{block_pattern}`
|
||||||
|
(everything up to the first wildcard character). Example: for
|
||||||
|
`20878594XX`, the prefix is `20878594` so `LIKE '20878594%'` catches
|
||||||
|
both the block and any carveouts.
|
||||||
|
|
||||||
|
## Step 2 — Categorize each match against the block
|
||||||
|
|
||||||
|
Walk the result rows and assign each pattern to one of:
|
||||||
|
|
||||||
|
- **Block itself**: pattern equals `{block_pattern}`. The "default"
|
||||||
|
routing for the block.
|
||||||
|
- **Carveout** (more-specific): pattern is fully inside the block but
|
||||||
|
contains fewer wildcards. Example: block `20878594XX`, carveout
|
||||||
|
`2087859498` (no wildcards) or `208785949[8-9]` (range narrower than
|
||||||
|
the block's `XX`).
|
||||||
|
- **Sibling block**: pattern shares the prefix but doesn't fall fully
|
||||||
|
inside the named block. Example: `2087859410` when the block is
|
||||||
|
`20878594XX` — wait, that IS inside; counter-example: `208785930X`
|
||||||
|
when the block is `20878594XX` shares `2087859` but not `20878594`.
|
||||||
|
- **Adjacent / unrelated**: shorter pattern, longer pattern, or one
|
||||||
|
whose prefix doesn't actually fall inside the block.
|
||||||
|
|
||||||
|
## Step 3 — Group by destination device
|
||||||
|
|
||||||
|
For each pattern in the block-itself + carveout categories, list its
|
||||||
|
destination device. The audit-grade output is:
|
||||||
|
|
||||||
|
```
|
||||||
|
Block: 20878594XX → RightFax-Trunk (100 DIDs nominally)
|
||||||
|
Carveouts that route differently:
|
||||||
|
2087859498 → ZetaFax-Trunk [single DID exception]
|
||||||
|
2087859499 → ZetaFax-Trunk [single DID exception]
|
||||||
|
Effective routing:
|
||||||
|
98 DIDs → RightFax-Trunk
|
||||||
|
2 DIDs → ZetaFax-Trunk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Findings to surface
|
||||||
|
|
||||||
|
- **Undocumented carveouts**: any carveout whose `description` doesn't
|
||||||
|
explain why it diverges from the block. High-severity if the
|
||||||
|
destination differs (silent routing exception); low if same-destination
|
||||||
|
(probably accidental duplication).
|
||||||
|
- **Same-destination carveouts**: patterns that route to the same device
|
||||||
|
as the block. Often harmless duplication that adds noise to the route
|
||||||
|
plan but no operational impact. Consolidation candidates.
|
||||||
|
- **Mixed-partition matches**: same pattern in multiple partitions can
|
||||||
|
produce different routing depending on the calling CSS. Flag if found
|
||||||
|
inside the audited block.
|
||||||
|
- **Disabled carveouts** (`np.blockenable = 't'`): a carveout that's
|
||||||
|
explicitly blocked is unusual — likely a temporary measure that wasn't
|
||||||
|
removed. Worth surfacing.
|
||||||
|
|
||||||
|
## Suggested follow-up calls
|
||||||
|
|
||||||
|
- `route_patterns_targeting('<destination>')` for each unique destination
|
||||||
|
device surfaced — confirms what *else* targets the same device beyond
|
||||||
|
this block.
|
||||||
|
- `route_inspect_pattern('<carveout pattern>')` to see the full route
|
||||||
|
trace for any carveout flagged as suspicious.
|
||||||
|
|
||||||
|
## Reference: longest-match semantics
|
||||||
|
|
||||||
|
""" + schema_block + """
|
||||||
|
|
||||||
|
Produce a structured report grouped by destination device, with the
|
||||||
|
"effective routing" summary at the top. Don't enumerate every digit —
|
||||||
|
summarize counts ("98 DIDs → A, 2 DIDs → B") and list only the carveout
|
||||||
|
patterns explicitly. Flag anything that looks like a silent override."""
|
||||||
144
src/mcaxl/prompts/partition_summary.py
Normal file
144
src/mcaxl/prompts/partition_summary.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
"""Partition orientation report — what is this partition actually for?
|
||||||
|
|
||||||
|
Composes existing tools (route_partitions, route_patterns) into a
|
||||||
|
"what does this partition do" narrative — pattern count, longest patterns,
|
||||||
|
distinct destination devices, internal vs external ratio. Useful when
|
||||||
|
you walk into a cluster and see a partition named `RTC-MGW-Inbound` or
|
||||||
|
similar and need to figure out its role before touching anything.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._common import render_schema_block
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..docs_loader import DocsIndex
|
||||||
|
|
||||||
|
|
||||||
|
_KEYWORDS = [
|
||||||
|
"route partition", "calling search space", "called party",
|
||||||
|
"route pattern", "translation pattern", "directory number",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render(docs: "DocsIndex | None", partition_name: str | None = None) -> str:
|
||||||
|
"""Compose a partition's role profile from existing routing data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
partition_name: the partition to profile. If None, the prompt
|
||||||
|
instructs the LLM to first list all partitions and pick the
|
||||||
|
most-populous to demonstrate the shape.
|
||||||
|
"""
|
||||||
|
schema_block = render_schema_block(
|
||||||
|
docs, _KEYWORDS, max_chunks=3, max_chars_per_chunk=700
|
||||||
|
)
|
||||||
|
|
||||||
|
target = partition_name or "<largest partition>"
|
||||||
|
discovery_step = (
|
||||||
|
""
|
||||||
|
if partition_name
|
||||||
|
else """## Step 0 — Pick a partition (no name supplied)
|
||||||
|
|
||||||
|
Run `route_partitions()` and pick the most-populous partition (highest
|
||||||
|
`pattern_count`) to demonstrate the profile shape. In production use,
|
||||||
|
the partition name should be supplied explicitly.
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""# Partition Orientation: `{target}`
|
||||||
|
|
||||||
|
Compose a "what is this partition for?" report from the routing data
|
||||||
|
already exposed by `route_partitions`, `route_patterns`, and
|
||||||
|
`route_calling_search_spaces`. No new SQL needed — this is an
|
||||||
|
LLM-orchestrated narrative across existing tools.
|
||||||
|
|
||||||
|
{discovery_step}## Step 1 — High-level profile via `route_partitions`
|
||||||
|
|
||||||
|
Call `route_partitions()` and find the row for `{target}`. Note:
|
||||||
|
|
||||||
|
- `pattern_count` — total numplan entries in the partition
|
||||||
|
- `css_count` — how many Calling Search Spaces include this partition
|
||||||
|
(signal of "blast radius" if you change the partition's contents)
|
||||||
|
|
||||||
|
If `css_count` is 0, the partition is **orphaned** — no calling device
|
||||||
|
can reach its patterns. High-severity audit finding (or vestigial cleanup
|
||||||
|
candidate). Surface it explicitly.
|
||||||
|
|
||||||
|
## Step 2 — Pattern inventory via `route_patterns(partition='{target}')`
|
||||||
|
|
||||||
|
Run `route_patterns(partition='{target}', limit=500)`. Categorize the
|
||||||
|
result by `kind`:
|
||||||
|
|
||||||
|
- **Directory numbers** (`tkpatternusage = 2`): user-assigned DNs.
|
||||||
|
Counted; if zero, partition holds no extensions.
|
||||||
|
- **Translation patterns** (`tkpatternusage = 3`): rewrite rules. List
|
||||||
|
the 5 longest patterns + 5 shortest.
|
||||||
|
- **Route patterns** (`tkpatternusage = 5`): outbound routing. Top
|
||||||
|
destinations by pattern count.
|
||||||
|
- **Hunt pilots** / **CTI route points** / etc.: count each.
|
||||||
|
|
||||||
|
Compute:
|
||||||
|
- **Internal vs external ratio**: count of patterns that look like 4-5
|
||||||
|
digit extensions vs 7+ digit external numbers.
|
||||||
|
- **Wildcard density**: count of patterns containing `X`, `!`, `@`,
|
||||||
|
`[…]` vs literal-only patterns.
|
||||||
|
- **Longest pattern**: indicates the partition's "deepest" rule.
|
||||||
|
|
||||||
|
## Step 3 — CSS membership via `route_calling_search_spaces`
|
||||||
|
|
||||||
|
Call `route_calling_search_spaces()` (no name filter) and find every
|
||||||
|
CSS whose ordered partition list contains `{target}`. For each:
|
||||||
|
|
||||||
|
- Position in the CSS (sortorder) — `0` means first-priority lookup
|
||||||
|
- Other partitions in the CSS — context for what the partition gets
|
||||||
|
paired with
|
||||||
|
|
||||||
|
A partition that's always at position 0 of its CSSes behaves as the
|
||||||
|
primary lookup; a partition always at the bottom is a fallback or
|
||||||
|
catch-all.
|
||||||
|
|
||||||
|
## Step 4 — Destination devices via `route_patterns_targeting`
|
||||||
|
|
||||||
|
For the route-pattern subset (Step 2), if there are ≤ 10 distinct
|
||||||
|
destination devices, call `route_patterns_targeting(<each device>)`
|
||||||
|
to see what *else* targets each. This catches the case where two
|
||||||
|
partitions both route to the same trunk — relevant for failover
|
||||||
|
analysis.
|
||||||
|
|
||||||
|
## Step 5 — Synthesize the role
|
||||||
|
|
||||||
|
Produce a 1-paragraph "what this partition is for" summary, then a
|
||||||
|
structured report:
|
||||||
|
|
||||||
|
```
|
||||||
|
Partition: <name>
|
||||||
|
Role inference: <e.g., "Internal extension home", "PSTN inbound
|
||||||
|
transformations", "Fax DID block">
|
||||||
|
Pattern count: N (M directory numbers, K route patterns, ...)
|
||||||
|
CSS membership: in N CSSes; primary in K, fallback in M
|
||||||
|
Internal/external mix: X% internal-shape, Y% external-shape
|
||||||
|
Wildcard density: N/M patterns use wildcards
|
||||||
|
Longest pattern: <pattern> (length=N)
|
||||||
|
Top 3 destinations: <list, if route patterns present>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Findings to call out
|
||||||
|
|
||||||
|
- **Orphaned partition** (CSS count = 0): not reachable from any device.
|
||||||
|
- **Single-DN partition**: holds 1 pattern only. Often vestigial; rarely
|
||||||
|
intentional.
|
||||||
|
- **Mixed-purpose**: contains a mix of internal extensions AND external
|
||||||
|
route patterns. Usually a sign of legacy migration; flag for review.
|
||||||
|
- **Position-inconsistent**: partition appears at sortorder 0 in some
|
||||||
|
CSSes and sortorder >5 in others. May indicate intentional priority
|
||||||
|
inversion or accidental misconfiguration — surface for operator review.
|
||||||
|
|
||||||
|
## Reference: partition + CSS semantics
|
||||||
|
|
||||||
|
""" + schema_block + """
|
||||||
|
|
||||||
|
Produce the structured report and the role-inference paragraph.
|
||||||
|
Don't dump every pattern — categorize, count, and call out exceptions."""
|
||||||
@ -546,6 +546,36 @@ def hunt_pilot_audit() -> str:
|
|||||||
return _prompts.hunt_pilot_audit.render(_docs)
|
return _prompts.hunt_pilot_audit.render(_docs)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt
|
||||||
|
def did_block_overlap(block_pattern: str) -> str:
|
||||||
|
"""Find carveout patterns inside a DID block — more-specific patterns
|
||||||
|
that win CUCM's longest-match rule and route differently than the
|
||||||
|
nominal block. Surfaces silent routing exceptions like "9498 and 9499
|
||||||
|
carved out of the 20878594XX block to route to a different fax server".
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block_pattern: the nominal block in CUCM pattern syntax, e.g.
|
||||||
|
``20878594XX`` or ``+1208524XXXX``.
|
||||||
|
"""
|
||||||
|
return _prompts.did_block_overlap.render(_docs, block_pattern)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt
|
||||||
|
def partition_summary(partition_name: str | None = None) -> str:
|
||||||
|
"""Compose a "what is this partition for?" report from existing
|
||||||
|
routing data — pattern count, longest patterns, distinct destination
|
||||||
|
devices, internal vs external ratio, CSS membership. Useful for
|
||||||
|
walking into a cluster and orienting on an unfamiliar partition
|
||||||
|
before touching anything.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
partition_name: partition to profile. If omitted, the prompt
|
||||||
|
instructs the LLM to first list partitions and pick the
|
||||||
|
most-populous one to demonstrate the shape.
|
||||||
|
"""
|
||||||
|
return _prompts.partition_summary.render(_docs, partition_name)
|
||||||
|
|
||||||
|
|
||||||
@mcp.prompt
|
@mcp.prompt
|
||||||
def whoami(userid: str | None = None) -> str:
|
def whoami(userid: str | None = None) -> str:
|
||||||
"""Look up the role chain for a single user (defaults to the AXL
|
"""Look up the role chain for a single user (defaults to the AXL
|
||||||
|
|||||||
@ -169,6 +169,8 @@ def test_all_prompts_registered_in_server():
|
|||||||
"inbound_did_audit",
|
"inbound_did_audit",
|
||||||
"hunt_pilot_audit",
|
"hunt_pilot_audit",
|
||||||
"whoami",
|
"whoami",
|
||||||
|
"did_block_overlap",
|
||||||
|
"partition_summary",
|
||||||
}, f"unexpected prompt set: {names}"
|
}, f"unexpected prompt set: {names}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user