Skip to content

Commit edaba4e

Browse files
committed
feat: Refactor ingestion and pack library routes
- Introduced `ingest_shared.py` for shared ingestion helpers, improving code organization and reusability. - Created `pack_library.py` to manage KnowledgePack CRUD operations, merging, and world application, separating concerns from `ingest.py`. - Enhanced frontend components for applying packs and displaying worlds with loading states. - Updated `SourcesPanel` to handle queued jobs and improved error handling in job listings. - Added tests for ingestion locking and pack serialization, ensuring robust functionality. - Updated pytest configuration for better test discovery and exclusion of unnecessary directories.
1 parent ec74a5e commit edaba4e

20 files changed

Lines changed: 3039 additions & 1580 deletions

File tree

packages/agents/src/monitor_agents/analyzer.py

Lines changed: 302 additions & 174 deletions
Large diffs are not rendered by default.

packages/agents/src/monitor_agents/canonkeeper.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -634,16 +634,26 @@ async def _commit_to_neo4j(
634634
return
635635

636636
_REL_TYPE_MAP = {
637-
"member_of": "MEMBER_OF", "part_of": "MEMBER_OF",
637+
"member_of": "MEMBER_OF",
638+
"part_of": "PART_OF",
639+
"subgroup_of": "SUBGROUP_OF",
640+
"affiliated_with": "AFFILIATED_WITH",
638641
"allied_with": "ALLIED_WITH",
639-
"enemy_of": "HOSTILE_TO", "opposes": "HOSTILE_TO", "hostile_to": "HOSTILE_TO",
642+
"enemy_of": "HOSTILE_TO",
643+
"opposes": "HOSTILE_TO",
644+
"hostile_to": "HOSTILE_TO",
640645
"owns": "OWNS",
641-
"located_in": "LOCATED_IN", "contains": "LOCATED_IN",
646+
"located_in": "LOCATED_IN",
647+
"contains": "CONTAINS",
642648
"leads": "LEADS",
643-
"serves": "WORKS_FOR", "controlled_by": "WORKS_FOR", "controls": "WORKS_FOR",
649+
"serves": "WORKS_FOR",
650+
"controlled_by": "CONTROLLED_BY",
651+
"controls": "CONTROLS",
644652
"works_for": "WORKS_FOR",
645-
"created_by": "DERIVES_FROM", "descends_from": "DERIVES_FROM",
646-
"derives_from": "DERIVES_FROM", "instance_of": "DERIVES_FROM",
653+
"created_by": "DERIVES_FROM",
654+
"descends_from": "DERIVES_FROM",
655+
"derives_from": "DERIVES_FROM",
656+
"instance_of": "INSTANCE_OF",
647657
"subtype_of": "SUBTYPE_OF",
648658
"worships": "REVERES",
649659
"participates_in": "PARTICIPATES_IN",

packages/agents/src/monitor_agents/parsers/analyzer_parsers.py

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -304,32 +304,63 @@ def parse_game_rules(reasoning: str) -> List[Dict[str, Any]]:
304304
# ---------------------------------------------------------------------------
305305

306306

307+
def _split_named_list(raw: str) -> List[str]:
308+
"""Split a comma/semicolon/bullet list of named system terms into clean items."""
309+
cleaned = raw.strip().strip("[]")
310+
cleaned = re.sub(r"\([^)]*\)", "", cleaned)
311+
cleaned = cleaned.replace("•", ",")
312+
items = [part.strip(" -\t") for part in re.split(r",|;", cleaned)]
313+
return [item[:100] for item in items if item and item.lower() not in {"none", "n/a", "na"}]
314+
315+
307316
def parse_character_sheet(reasoning: str) -> Dict[str, Any]:
308-
"""
309-
Parse DSPy character sheet output lines.
310-
Format:
311-
ATTR | name | abbrev | min | max | default | modifier_formula
312-
SKILL | name | linked_attribute | description
313-
RESOURCE | name | abbrev | min | recovers_on | depleted_effect
314-
MECHANIC | type | formula | success_type
315-
"""
317+
"""Parse structured or semi-structured system-schema output from the analyzer prompt."""
316318
attributes: List[Dict[str, Any]] = []
317319
skills: List[Dict[str, Any]] = []
318320
resources: List[Dict[str, Any]] = []
321+
powers: List[Dict[str, Any]] = []
322+
subsystems: List[Dict[str, Any]] = []
319323
core_mechanic: Dict[str, Any] = {}
320324

321-
for line in reasoning.splitlines():
322-
line = line.strip()
325+
attr_aliases = {"ATTR", "ATTRIBUTE", "ATTRIBUTES", "ABILITY", "ABILITIES", "STAT", "STATS", "TRAIT", "TRAITS"}
326+
skill_aliases = {"SKILL", "SKILLS", "KNACK", "KNACKS", "APPROACH", "APPROACHES"}
327+
resource_aliases = {"RESOURCE", "RESOURCES", "TRACK", "TRACKS"}
328+
power_aliases = {"POWER", "POWERS", "DISCIPLINE", "DISCIPLINES", "ARCANA", "SCHOOL", "SCHOOLS", "EDGE", "EDGES", "FEAT", "FEATS", "MERIT", "MERITS", "ADVANTAGE", "ADVANTAGES"}
329+
subsystem_aliases = {"SUBSYSTEM", "SUBSYSTEMS", "MODULE", "MODULES", "MODE", "MODES"}
330+
mechanic_aliases = {"MECHANIC", "MECHANICS", "CORE_MECHANIC", "SYSTEM"}
331+
332+
for raw_line in reasoning.splitlines():
333+
line = raw_line.strip()
323334
if not line:
324335
continue
325-
parts = [p.strip() for p in line.split("|")]
336+
337+
# Accept human-readable heading lines like "Attributes: Brawn, Finesse, Wits"
338+
if "|" not in line and ":" in line:
339+
heading, body = line.split(":", 1)
340+
heading_key = heading.strip().upper().replace(" ", "_")
341+
parts = [heading_key, body.strip()]
342+
else:
343+
parts = [p.strip() for p in line.split("|")]
344+
326345
keyword = parts[0].upper() if parts else ""
327346

328-
if keyword == "ATTR" and len(parts) >= 3:
347+
if keyword in attr_aliases and len(parts) >= 2:
329348
try:
349+
if len(parts) == 2 or (len(parts) <= 3 and any(sep in parts[1] for sep in [",", ";"])):
350+
for name in _split_named_list(" ".join(parts[1:])):
351+
attributes.append({
352+
"name": name,
353+
"abbreviation": name[:10].upper(),
354+
"min_value": 1,
355+
"max_value": 20,
356+
"default_value": 10,
357+
"modifier_formula": None,
358+
})
359+
continue
360+
330361
attributes.append({
331362
"name": parts[1][:100],
332-
"abbreviation": parts[2][:10] if len(parts) > 2 else parts[1][:10],
363+
"abbreviation": parts[2][:10] if len(parts) > 2 and parts[2] else parts[1][:10].upper(),
333364
"min_value": int(parts[3]) if len(parts) > 3 and parts[3].lstrip("-").isdigit() else 1,
334365
"max_value": int(parts[4]) if len(parts) > 4 and parts[4].lstrip("-").isdigit() else 20,
335366
"default_value": int(parts[5]) if len(parts) > 5 and parts[5].lstrip("-").isdigit() else 10,
@@ -338,33 +369,84 @@ def parse_character_sheet(reasoning: str) -> Dict[str, Any]:
338369
except (ValueError, IndexError):
339370
continue
340371

341-
elif keyword == "SKILL" and len(parts) >= 2:
372+
elif keyword in skill_aliases and len(parts) >= 2:
342373
try:
374+
if len(parts) == 2 or (len(parts) <= 3 and any(sep in parts[1] for sep in [",", ";"])):
375+
for name in _split_named_list(" ".join(parts[1:])):
376+
skills.append({
377+
"name": name,
378+
"linked_attribute": None,
379+
"description": None,
380+
})
381+
continue
382+
343383
skills.append({
344384
"name": parts[1][:100],
345385
"linked_attribute": parts[2][:100] if len(parts) > 2 and parts[2] else None,
346-
"description": parts[3][:500] if len(parts) > 3 else None,
386+
"description": parts[3][:500] if len(parts) > 3 and parts[3] else None,
347387
})
348388
except (ValueError, IndexError):
349389
continue
350390

351-
elif keyword == "RESOURCE" and len(parts) >= 2:
391+
elif keyword in resource_aliases and len(parts) >= 2:
352392
try:
393+
if len(parts) == 2 or (len(parts) <= 3 and any(sep in parts[1] for sep in [",", ";"])):
394+
for name in _split_named_list(" ".join(parts[1:])):
395+
resources.append({
396+
"name": name,
397+
"abbreviation": name[:10].upper(),
398+
"min_value": 0,
399+
"recovers_on": None,
400+
"depleted_effect": None,
401+
"calculation": None,
402+
})
403+
continue
404+
353405
resources.append({
354406
"name": parts[1][:100],
355-
"abbreviation": parts[2][:10] if len(parts) > 2 and parts[2] else parts[1][:10],
407+
"abbreviation": parts[2][:10] if len(parts) > 2 and parts[2] else parts[1][:10].upper(),
356408
"min_value": int(parts[3]) if len(parts) > 3 and parts[3].lstrip("-").isdigit() else 0,
357409
"recovers_on": parts[4][:100] if len(parts) > 4 and parts[4] else None,
358410
"depleted_effect": parts[5][:200] if len(parts) > 5 and parts[5] else None,
411+
"calculation": parts[6][:200] if len(parts) > 6 and parts[6] else None,
412+
})
413+
except (ValueError, IndexError):
414+
continue
415+
416+
elif keyword in power_aliases and len(parts) >= 2:
417+
try:
418+
if len(parts) == 2 or (len(parts) <= 3 and any(sep in parts[1] for sep in [",", ";"])):
419+
for name in _split_named_list(" ".join(parts[1:])):
420+
powers.append({
421+
"name": name,
422+
"category": "power",
423+
"description": None,
424+
})
425+
continue
426+
427+
powers.append({
428+
"name": parts[1][:100],
429+
"category": parts[2][:100] if len(parts) > 2 and parts[2] else "power",
430+
"description": parts[3][:500] if len(parts) > 3 and parts[3] else None,
431+
})
432+
except (ValueError, IndexError):
433+
continue
434+
435+
elif keyword in subsystem_aliases and len(parts) >= 2:
436+
try:
437+
subsystems.append({
438+
"name": parts[1][:100],
439+
"scope": parts[2][:100] if len(parts) > 2 and parts[2] else "custom",
440+
"description": parts[3][:500] if len(parts) > 3 and parts[3] else None,
359441
})
360442
except (ValueError, IndexError):
361443
continue
362444

363-
elif keyword == "MECHANIC" and len(parts) >= 2 and not core_mechanic:
445+
elif keyword in mechanic_aliases and len(parts) >= 2 and not core_mechanic:
364446
try:
365447
core_mechanic = {
366448
"type": parts[1].lower() if len(parts) > 1 else "d20",
367-
"formula": parts[2][:200] if len(parts) > 2 and parts[2] else "Roll and compare",
449+
"formula": parts[2][:200] if len(parts) > 2 and parts[2] else (parts[1][:200] if len(parts) == 2 else "Roll and compare"),
368450
"success_type": parts[3].lower() if len(parts) > 3 and parts[3] else "meet_or_beat",
369451
}
370452
except (ValueError, IndexError):
@@ -374,6 +456,8 @@ def parse_character_sheet(reasoning: str) -> Dict[str, Any]:
374456
"attributes": attributes,
375457
"skills": skills,
376458
"resources": resources,
459+
"powers": powers,
460+
"subsystems": subsystems,
377461
"core_mechanic": core_mechanic,
378462
}
379463

0 commit comments

Comments
 (0)