yamlgraph 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
examples/npc/demo.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Automated demo of the D&D NPC Encounter System using YAMLGraph.
|
|
3
|
+
|
|
4
|
+
This script demonstrates:
|
|
5
|
+
1. Creating NPCs using the npc-creation.yaml graph
|
|
6
|
+
2. Running automated encounters with pre-scripted scenarios
|
|
7
|
+
3. Optionally generating images for each turn
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python examples/npc/demo.py # 3 NPCs, 5 rounds
|
|
11
|
+
python examples/npc/demo.py --npcs 2 # 2 NPCs
|
|
12
|
+
python examples/npc/demo.py --rounds 3 # 3 rounds
|
|
13
|
+
python examples/npc/demo.py --images # Generate images
|
|
14
|
+
python examples/npc/demo.py -n 4 -r 3 -i # 4 NPCs, 3 rounds, images
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import random
|
|
19
|
+
import uuid
|
|
20
|
+
|
|
21
|
+
from langgraph.types import Command
|
|
22
|
+
|
|
23
|
+
from yamlgraph.graph_loader import (
|
|
24
|
+
compile_graph,
|
|
25
|
+
get_checkpointer_for_graph,
|
|
26
|
+
load_graph_config,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# Colors for output
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class C:
|
|
35
|
+
RESET = "\033[0m"
|
|
36
|
+
BOLD = "\033[1m"
|
|
37
|
+
DIM = "\033[2m"
|
|
38
|
+
CYAN = "\033[36m"
|
|
39
|
+
GREEN = "\033[32m"
|
|
40
|
+
YELLOW = "\033[33m"
|
|
41
|
+
MAGENTA = "\033[35m"
|
|
42
|
+
BLUE = "\033[34m"
|
|
43
|
+
RED = "\033[31m"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def header(text: str):
|
|
47
|
+
print(f"\n{C.BOLD}{C.CYAN}{'═' * 60}{C.RESET}")
|
|
48
|
+
print(f"{C.BOLD}{C.CYAN} {text}{C.RESET}")
|
|
49
|
+
print(f"{C.BOLD}{C.CYAN}{'═' * 60}{C.RESET}\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def section(text: str):
|
|
53
|
+
print(f"\n{C.YELLOW}▶ {text}{C.RESET}")
|
|
54
|
+
print(f"{C.DIM}{'─' * 50}{C.RESET}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# NPC Concepts for Demo
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
NPC_CONCEPTS = [
|
|
62
|
+
{
|
|
63
|
+
"concept": "a gruff dwarven bartender who secretly waters down the ale",
|
|
64
|
+
"race": "Dwarf",
|
|
65
|
+
"character_class": "Commoner",
|
|
66
|
+
"location": "The Red Dragon Inn",
|
|
67
|
+
"role_in_story": "Bartender who knows everyone's secrets",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"concept": "a mysterious elven bard who trades in information and secrets",
|
|
71
|
+
"race": "Elf",
|
|
72
|
+
"character_class": "Bard",
|
|
73
|
+
"location": "The Red Dragon Inn",
|
|
74
|
+
"role_in_story": "Information broker",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"concept": "a retired human soldier turned bouncer, haunted by the past",
|
|
78
|
+
"race": "Human",
|
|
79
|
+
"character_class": "Fighter",
|
|
80
|
+
"location": "The Red Dragon Inn",
|
|
81
|
+
"role_in_story": "Tavern bouncer and protector",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"concept": "a young tiefling server saving money for wizard academy",
|
|
85
|
+
"race": "Tiefling",
|
|
86
|
+
"character_class": "Commoner",
|
|
87
|
+
"location": "The Red Dragon Inn",
|
|
88
|
+
"role_in_story": "Aspiring wizard working as server",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"concept": "a paranoid gnome inventor convinced everyone wants his secrets",
|
|
92
|
+
"race": "Gnome",
|
|
93
|
+
"character_class": "Artificer",
|
|
94
|
+
"location": "The Red Dragon Inn",
|
|
95
|
+
"role_in_story": "Eccentric patron with gadgets",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"concept": "a boisterous half-orc celebrating their first dungeon delve",
|
|
99
|
+
"race": "Half-Orc",
|
|
100
|
+
"character_class": "Barbarian",
|
|
101
|
+
"location": "The Red Dragon Inn",
|
|
102
|
+
"role_in_story": "Novice adventurer patron",
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Pre-scripted DM scenarios
|
|
107
|
+
DM_SCENARIOS = [
|
|
108
|
+
"A group of adventurers bursts through the door, looking exhausted. One shouts 'We need help! There's something in the mines!'",
|
|
109
|
+
"The adventurers explain they barely escaped strange creatures in the old silver mine. They need supplies and information.",
|
|
110
|
+
"One adventurer slams a glowing crystal on the bar and asks if anyone knows what it is.",
|
|
111
|
+
"A hooded figure in the corner stands and offers to buy the crystal for 500 gold.",
|
|
112
|
+
"The city watch enters, looking for someone matching one adventurer's description.",
|
|
113
|
+
"A loud explosion rocks the building. Through the windows, smoke rises from the market.",
|
|
114
|
+
"One adventurer collapses, muttering about 'the shadow beneath' in delirium.",
|
|
115
|
+
"A young child runs in crying - their parents didn't come home from the mines.",
|
|
116
|
+
"The hooded figure reveals themselves as a noble's agent and demands cooperation.",
|
|
117
|
+
"Strange scratching sounds begin coming from the cellar below.",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# Demo Functions
|
|
123
|
+
# =============================================================================
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_npc(concept_data: dict, index: int) -> dict:
|
|
127
|
+
"""Create an NPC using the npc-creation graph."""
|
|
128
|
+
section(f"Creating NPC {index + 1}: {concept_data.get('role_in_story', 'NPC')}")
|
|
129
|
+
|
|
130
|
+
print(f" 📝 Concept: {C.DIM}{concept_data['concept'][:60]}...{C.RESET}")
|
|
131
|
+
print(" ⏳ Generating NPC...")
|
|
132
|
+
|
|
133
|
+
# Build and run the NPC creation graph
|
|
134
|
+
config = load_graph_config("examples/npc/npc-creation.yaml")
|
|
135
|
+
graph = compile_graph(config)
|
|
136
|
+
app = graph.compile()
|
|
137
|
+
|
|
138
|
+
result = app.invoke(concept_data)
|
|
139
|
+
|
|
140
|
+
# Extract NPC data from result
|
|
141
|
+
identity = result.get("identity", {})
|
|
142
|
+
if hasattr(identity, "model_dump"):
|
|
143
|
+
identity = identity.model_dump()
|
|
144
|
+
|
|
145
|
+
npc_name = identity.get("name", f"NPC_{index}")
|
|
146
|
+
npc_race = identity.get("race", concept_data.get("race", "Unknown"))
|
|
147
|
+
npc_class = identity.get("character_class", concept_data.get("character_class", ""))
|
|
148
|
+
|
|
149
|
+
print(f" ✓ Created: {C.GREEN}{C.BOLD}{npc_name}{C.RESET}")
|
|
150
|
+
print(f" {npc_race} {npc_class}")
|
|
151
|
+
|
|
152
|
+
# Build flat NPC dict for encounter
|
|
153
|
+
personality = result.get("personality", {})
|
|
154
|
+
if hasattr(personality, "model_dump"):
|
|
155
|
+
personality = personality.model_dump()
|
|
156
|
+
|
|
157
|
+
behavior = result.get("behavior", {})
|
|
158
|
+
if hasattr(behavior, "model_dump"):
|
|
159
|
+
behavior = behavior.model_dump()
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"name": npc_name,
|
|
163
|
+
"race": str(npc_race),
|
|
164
|
+
"character_class": str(npc_class),
|
|
165
|
+
"appearance": identity.get("appearance", ""),
|
|
166
|
+
"voice": identity.get("voice", ""),
|
|
167
|
+
"personality": str(personality),
|
|
168
|
+
"behavior": str(behavior),
|
|
169
|
+
"goals": str(behavior.get("goals", [])),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def create_multiple_npcs(count: int) -> list[dict]:
|
|
174
|
+
"""Create multiple NPCs for the demo."""
|
|
175
|
+
header(f"Creating {count} NPCs")
|
|
176
|
+
|
|
177
|
+
concepts = random.sample(NPC_CONCEPTS, min(count, len(NPC_CONCEPTS)))
|
|
178
|
+
npcs = []
|
|
179
|
+
|
|
180
|
+
for i, concept in enumerate(concepts):
|
|
181
|
+
try:
|
|
182
|
+
npc = create_npc(concept, i)
|
|
183
|
+
npcs.append(npc)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f" {C.RED}✗ Error: {e}{C.RESET}")
|
|
186
|
+
|
|
187
|
+
return npcs
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def show_npc_roster(npcs: list[dict]):
|
|
191
|
+
"""Display the NPC roster."""
|
|
192
|
+
section(f"NPC Roster ({len(npcs)} characters)")
|
|
193
|
+
|
|
194
|
+
for i, npc in enumerate(npcs, 1):
|
|
195
|
+
print(f" {C.CYAN}{i}. {C.BOLD}{npc['name']}{C.RESET}")
|
|
196
|
+
print(f" {npc['race']} {npc['character_class']}")
|
|
197
|
+
print()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_automated_encounter(
|
|
201
|
+
npcs: list[dict],
|
|
202
|
+
num_rounds: int = 5,
|
|
203
|
+
generate_images: bool = False,
|
|
204
|
+
):
|
|
205
|
+
"""Run an automated encounter with pre-scripted DM inputs."""
|
|
206
|
+
header(f"Running Automated Encounter ({num_rounds} rounds)")
|
|
207
|
+
|
|
208
|
+
print(" 📍 Location: The Red Dragon Inn")
|
|
209
|
+
print(f" 🎭 NPCs: {', '.join(npc['name'] for npc in npcs)}")
|
|
210
|
+
print(f" 🔄 Rounds: {num_rounds}")
|
|
211
|
+
if generate_images:
|
|
212
|
+
print(" 🖼️ Images: Enabled")
|
|
213
|
+
|
|
214
|
+
# Load encounter graph
|
|
215
|
+
config = load_graph_config("examples/npc/encounter-multi.yaml")
|
|
216
|
+
graph = compile_graph(config)
|
|
217
|
+
checkpointer = get_checkpointer_for_graph(config)
|
|
218
|
+
app = graph.compile(checkpointer=checkpointer)
|
|
219
|
+
|
|
220
|
+
# Session setup
|
|
221
|
+
thread_id = str(uuid.uuid4())
|
|
222
|
+
run_config = {"configurable": {"thread_id": thread_id}}
|
|
223
|
+
|
|
224
|
+
# Initial state
|
|
225
|
+
initial_state = {
|
|
226
|
+
"npcs": npcs,
|
|
227
|
+
"location": "The Red Dragon Inn",
|
|
228
|
+
"location_description": "A warm, crowded tavern with a crackling fireplace and the smell of ale",
|
|
229
|
+
"turn_number": 1,
|
|
230
|
+
"encounter_history": [],
|
|
231
|
+
"perceptions": [],
|
|
232
|
+
"decisions": [],
|
|
233
|
+
"narrations": [],
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Start the graph (will hit interrupt)
|
|
237
|
+
app.invoke(initial_state, run_config)
|
|
238
|
+
|
|
239
|
+
# Run through scenarios
|
|
240
|
+
scenarios = DM_SCENARIOS[:num_rounds]
|
|
241
|
+
|
|
242
|
+
for turn_num, dm_input in enumerate(scenarios, 1):
|
|
243
|
+
print(f"\n{C.BOLD}{'═' * 60}{C.RESET}")
|
|
244
|
+
print(f"{C.BOLD} Turn {turn_num} of {num_rounds}{C.RESET}")
|
|
245
|
+
print(f"{C.BOLD}{'═' * 60}{C.RESET}")
|
|
246
|
+
|
|
247
|
+
print(f'\n{C.CYAN}DM:{C.RESET} "{dm_input}"\n')
|
|
248
|
+
print(f"{C.DIM}Processing all NPCs...{C.RESET}")
|
|
249
|
+
|
|
250
|
+
# Resume with DM input
|
|
251
|
+
result = app.invoke(Command(resume=dm_input), run_config)
|
|
252
|
+
|
|
253
|
+
# Show turn summary
|
|
254
|
+
if result.get("turn_summary"):
|
|
255
|
+
print(f"\n{C.GREEN}📜 Turn Summary:{C.RESET}")
|
|
256
|
+
summary = result["turn_summary"]
|
|
257
|
+
if hasattr(summary, "model_dump"):
|
|
258
|
+
summary = str(summary)
|
|
259
|
+
# Wrap long text
|
|
260
|
+
for line in str(summary).split("\n"):
|
|
261
|
+
print(f" {line}")
|
|
262
|
+
|
|
263
|
+
# Show image if generated
|
|
264
|
+
if result.get("scene_image"):
|
|
265
|
+
print(f"\n{C.CYAN}🖼️ Image: {result['scene_image']}{C.RESET}")
|
|
266
|
+
|
|
267
|
+
# Final summary
|
|
268
|
+
header("Encounter Complete!")
|
|
269
|
+
|
|
270
|
+
history = result.get("encounter_history", [])
|
|
271
|
+
if history:
|
|
272
|
+
print(f"{C.CYAN}📚 {len(history)} turns recorded in history{C.RESET}")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def main():
|
|
276
|
+
parser = argparse.ArgumentParser(
|
|
277
|
+
description="Automated D&D NPC Encounter Demo using YAMLGraph"
|
|
278
|
+
)
|
|
279
|
+
parser.add_argument(
|
|
280
|
+
"--npcs",
|
|
281
|
+
"-n",
|
|
282
|
+
type=int,
|
|
283
|
+
default=3,
|
|
284
|
+
help="Number of NPCs to create (default: 3)",
|
|
285
|
+
)
|
|
286
|
+
parser.add_argument(
|
|
287
|
+
"--rounds",
|
|
288
|
+
"-r",
|
|
289
|
+
type=int,
|
|
290
|
+
default=5,
|
|
291
|
+
help="Number of encounter rounds (default: 5)",
|
|
292
|
+
)
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--images",
|
|
295
|
+
"-i",
|
|
296
|
+
action="store_true",
|
|
297
|
+
help="Generate images for each turn (requires REPLICATE_API_TOKEN)",
|
|
298
|
+
)
|
|
299
|
+
parser.add_argument(
|
|
300
|
+
"--skip-creation",
|
|
301
|
+
action="store_true",
|
|
302
|
+
help="Skip NPC creation, use default NPCs",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
args = parser.parse_args()
|
|
306
|
+
|
|
307
|
+
header("D&D NPC ENCOUNTER SYSTEM - Automated Demo")
|
|
308
|
+
|
|
309
|
+
print("This demo shows AI-powered NPCs responding to pre-scripted scenarios.")
|
|
310
|
+
print("The AI perceives each scene, decides actions, and narrates in character.")
|
|
311
|
+
print()
|
|
312
|
+
|
|
313
|
+
# Get NPCs
|
|
314
|
+
if args.skip_creation:
|
|
315
|
+
print(f"{C.YELLOW}Using default NPCs...{C.RESET}")
|
|
316
|
+
npcs = [
|
|
317
|
+
{
|
|
318
|
+
"name": "Thorin Ironfoot",
|
|
319
|
+
"race": "Dwarf",
|
|
320
|
+
"character_class": "Blacksmith",
|
|
321
|
+
"appearance": "Stocky with burn scars and a copper beard",
|
|
322
|
+
"voice": "Gruff, low rumble",
|
|
323
|
+
"personality": {
|
|
324
|
+
"traits": ["Stoic", "Hardworking", "Practical"],
|
|
325
|
+
"ideals": ["Quality craftsmanship", "Honest trade"],
|
|
326
|
+
"flaws": ["Distrustful of magic", "Stubborn"],
|
|
327
|
+
"disposition": "neutral",
|
|
328
|
+
},
|
|
329
|
+
"behavior": {
|
|
330
|
+
"combat_style": "defensive",
|
|
331
|
+
"goals": ["Run a successful smithy", "Protect the tavern"],
|
|
332
|
+
},
|
|
333
|
+
"goals": ["Run a successful smithy", "Protect the tavern"],
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "Lyra Whisperwind",
|
|
337
|
+
"race": "Elf",
|
|
338
|
+
"character_class": "Ranger",
|
|
339
|
+
"appearance": "Lithe with silver hair and piercing blue eyes",
|
|
340
|
+
"voice": "Melodic, speaks in riddles",
|
|
341
|
+
"personality": {
|
|
342
|
+
"traits": ["Curious", "Mischievous", "Observant"],
|
|
343
|
+
"ideals": ["Truth", "Knowledge"],
|
|
344
|
+
"flaws": ["Overly secretive", "Distrustful"],
|
|
345
|
+
"disposition": "curious",
|
|
346
|
+
},
|
|
347
|
+
"behavior": {
|
|
348
|
+
"combat_style": "ranged",
|
|
349
|
+
"goals": ["Uncover hidden truths", "Gather information"],
|
|
350
|
+
},
|
|
351
|
+
"goals": ["Uncover hidden truths", "Gather information"],
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"name": "Marcus the Bold",
|
|
355
|
+
"race": "Human",
|
|
356
|
+
"character_class": "Fighter",
|
|
357
|
+
"appearance": "Burly with thick beard and battle scars",
|
|
358
|
+
"voice": "Booming laugh",
|
|
359
|
+
"personality": {
|
|
360
|
+
"traits": ["Jovial", "Brave", "Loyal"],
|
|
361
|
+
"ideals": ["Glory", "Protecting the weak"],
|
|
362
|
+
"flaws": ["Reckless", "Overconfident"],
|
|
363
|
+
"disposition": "friendly",
|
|
364
|
+
},
|
|
365
|
+
"behavior": {
|
|
366
|
+
"combat_style": "aggressive",
|
|
367
|
+
"goals": ["Find glory in battle", "Protect friends"],
|
|
368
|
+
},
|
|
369
|
+
"goals": ["Find glory in battle", "Protect friends"],
|
|
370
|
+
},
|
|
371
|
+
][: args.npcs]
|
|
372
|
+
else:
|
|
373
|
+
npcs = create_multiple_npcs(args.npcs)
|
|
374
|
+
|
|
375
|
+
if not npcs:
|
|
376
|
+
print(f"{C.RED}No NPCs available. Exiting.{C.RESET}")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
# Show roster
|
|
380
|
+
show_npc_roster(npcs)
|
|
381
|
+
|
|
382
|
+
# Run encounter
|
|
383
|
+
run_automated_encounter(npcs, args.rounds, args.images)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
main()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""NPC encounter node for generating scene images.
|
|
2
|
+
|
|
3
|
+
This node generates an image for each turn of the encounter.
|
|
4
|
+
Uses the shared replicate_tool from examples/shared.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from examples.shared.replicate_tool import ImageResult, generate_image
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Type alias for state
|
|
19
|
+
GraphState = dict[str, Any]
|
|
20
|
+
|
|
21
|
+
# Output directory for generated images
|
|
22
|
+
OUTPUT_DIR = Path("outputs/npc/encounters")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_scene_image_node(state: GraphState) -> dict:
|
|
26
|
+
"""Generate an image for the current encounter scene.
|
|
27
|
+
|
|
28
|
+
Reads the scene_prompt from state (generated by describe_scene LLM node)
|
|
29
|
+
and generates an image using the shared replicate tool.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
state: Graph state with 'scene_prompt' containing the image prompt
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
State update with 'scene_image' path and metadata
|
|
36
|
+
"""
|
|
37
|
+
scene_prompt = state.get("scene_prompt")
|
|
38
|
+
if not scene_prompt:
|
|
39
|
+
logger.warning("No scene_prompt in state, skipping image generation")
|
|
40
|
+
return {
|
|
41
|
+
"current_step": "generate_scene_image",
|
|
42
|
+
"scene_image": None,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Handle Pydantic model or dict
|
|
46
|
+
if hasattr(scene_prompt, "model_dump"):
|
|
47
|
+
prompt_text = scene_prompt.model_dump().get("prompt", str(scene_prompt))
|
|
48
|
+
elif isinstance(scene_prompt, dict):
|
|
49
|
+
prompt_text = scene_prompt.get("prompt", str(scene_prompt))
|
|
50
|
+
else:
|
|
51
|
+
prompt_text = str(scene_prompt)
|
|
52
|
+
|
|
53
|
+
# Add style suffix for consistent fantasy look
|
|
54
|
+
style_suffix = (
|
|
55
|
+
"Fantasy illustration style, dramatic lighting, "
|
|
56
|
+
"detailed, vibrant colors, D&D inspired, painterly"
|
|
57
|
+
)
|
|
58
|
+
full_prompt = f"{prompt_text}. {style_suffix}"
|
|
59
|
+
|
|
60
|
+
# Create output directory with timestamp
|
|
61
|
+
turn_number = state.get("turn_number", 1)
|
|
62
|
+
npc_name = state.get("npc_name", "unknown").replace(" ", "_").lower()
|
|
63
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
64
|
+
|
|
65
|
+
output_dir = OUTPUT_DIR / f"{npc_name}_{timestamp}"
|
|
66
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
# Generate image
|
|
69
|
+
output_path = output_dir / f"turn_{turn_number:02d}.webp"
|
|
70
|
+
|
|
71
|
+
logger.info(f"🎨 Generating scene image: {output_path}")
|
|
72
|
+
logger.debug(f"Prompt: {full_prompt[:100]}...")
|
|
73
|
+
|
|
74
|
+
result: ImageResult = generate_image(
|
|
75
|
+
prompt=full_prompt,
|
|
76
|
+
output_path=output_path,
|
|
77
|
+
model_name="hidream", # Better for fantasy/illustration style
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if result.success:
|
|
81
|
+
logger.info(f"✅ Generated image: {result.path}")
|
|
82
|
+
return {
|
|
83
|
+
"current_step": "generate_scene_image",
|
|
84
|
+
"scene_image": str(result.path),
|
|
85
|
+
}
|
|
86
|
+
else:
|
|
87
|
+
logger.error(f"❌ Image generation failed: {result.error}")
|
|
88
|
+
return {
|
|
89
|
+
"current_step": "generate_scene_image",
|
|
90
|
+
"scene_image": None,
|
|
91
|
+
"image_error": result.error,
|
|
92
|
+
}
|