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.
Files changed (185) hide show
  1. examples/__init__.py +1 -0
  2. examples/codegen/__init__.py +5 -0
  3. examples/codegen/models/__init__.py +13 -0
  4. examples/codegen/models/schemas.py +76 -0
  5. examples/codegen/tests/__init__.py +1 -0
  6. examples/codegen/tests/test_ai_helpers.py +235 -0
  7. examples/codegen/tests/test_ast_analysis.py +174 -0
  8. examples/codegen/tests/test_code_analysis.py +134 -0
  9. examples/codegen/tests/test_code_context.py +301 -0
  10. examples/codegen/tests/test_code_nav.py +89 -0
  11. examples/codegen/tests/test_dependency_tools.py +119 -0
  12. examples/codegen/tests/test_example_tools.py +185 -0
  13. examples/codegen/tests/test_git_tools.py +112 -0
  14. examples/codegen/tests/test_impl_agent_schemas.py +193 -0
  15. examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
  16. examples/codegen/tests/test_jedi_analysis.py +226 -0
  17. examples/codegen/tests/test_meta_tools.py +250 -0
  18. examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
  19. examples/codegen/tests/test_syntax_tools.py +85 -0
  20. examples/codegen/tests/test_synthesize_prompt.py +94 -0
  21. examples/codegen/tests/test_template_tools.py +244 -0
  22. examples/codegen/tools/__init__.py +80 -0
  23. examples/codegen/tools/ai_helpers.py +420 -0
  24. examples/codegen/tools/ast_analysis.py +92 -0
  25. examples/codegen/tools/code_context.py +180 -0
  26. examples/codegen/tools/code_nav.py +52 -0
  27. examples/codegen/tools/dependency_tools.py +120 -0
  28. examples/codegen/tools/example_tools.py +188 -0
  29. examples/codegen/tools/git_tools.py +151 -0
  30. examples/codegen/tools/impl_executor.py +614 -0
  31. examples/codegen/tools/jedi_analysis.py +311 -0
  32. examples/codegen/tools/meta_tools.py +202 -0
  33. examples/codegen/tools/syntax_tools.py +26 -0
  34. examples/codegen/tools/template_tools.py +356 -0
  35. examples/fastapi_interview.py +167 -0
  36. examples/npc/api/__init__.py +1 -0
  37. examples/npc/api/app.py +100 -0
  38. examples/npc/api/routes/__init__.py +5 -0
  39. examples/npc/api/routes/encounter.py +182 -0
  40. examples/npc/api/session.py +330 -0
  41. examples/npc/demo.py +387 -0
  42. examples/npc/nodes/__init__.py +5 -0
  43. examples/npc/nodes/image_node.py +92 -0
  44. examples/npc/run_encounter.py +230 -0
  45. examples/shared/__init__.py +0 -0
  46. examples/shared/replicate_tool.py +238 -0
  47. examples/storyboard/__init__.py +1 -0
  48. examples/storyboard/generate_videos.py +335 -0
  49. examples/storyboard/nodes/__init__.py +12 -0
  50. examples/storyboard/nodes/animated_character_node.py +248 -0
  51. examples/storyboard/nodes/animated_image_node.py +138 -0
  52. examples/storyboard/nodes/character_node.py +162 -0
  53. examples/storyboard/nodes/image_node.py +118 -0
  54. examples/storyboard/nodes/replicate_tool.py +49 -0
  55. examples/storyboard/retry_images.py +118 -0
  56. scripts/demo_async_executor.py +212 -0
  57. scripts/demo_interview_e2e.py +200 -0
  58. scripts/demo_streaming.py +140 -0
  59. scripts/run_interview_demo.py +94 -0
  60. scripts/test_interrupt_fix.py +26 -0
  61. tests/__init__.py +1 -0
  62. tests/conftest.py +178 -0
  63. tests/integration/__init__.py +1 -0
  64. tests/integration/test_animated_storyboard.py +63 -0
  65. tests/integration/test_cli_commands.py +242 -0
  66. tests/integration/test_colocated_prompts.py +139 -0
  67. tests/integration/test_map_demo.py +50 -0
  68. tests/integration/test_memory_demo.py +283 -0
  69. tests/integration/test_npc_api/__init__.py +1 -0
  70. tests/integration/test_npc_api/test_routes.py +357 -0
  71. tests/integration/test_npc_api/test_session.py +216 -0
  72. tests/integration/test_pipeline_flow.py +105 -0
  73. tests/integration/test_providers.py +163 -0
  74. tests/integration/test_resume.py +75 -0
  75. tests/integration/test_subgraph_integration.py +295 -0
  76. tests/integration/test_subgraph_interrupt.py +106 -0
  77. tests/unit/__init__.py +1 -0
  78. tests/unit/test_agent_nodes.py +355 -0
  79. tests/unit/test_async_executor.py +346 -0
  80. tests/unit/test_checkpointer.py +212 -0
  81. tests/unit/test_checkpointer_factory.py +212 -0
  82. tests/unit/test_cli.py +121 -0
  83. tests/unit/test_cli_package.py +81 -0
  84. tests/unit/test_compile_graph_map.py +132 -0
  85. tests/unit/test_conditions_routing.py +253 -0
  86. tests/unit/test_config.py +93 -0
  87. tests/unit/test_conversation_memory.py +276 -0
  88. tests/unit/test_database.py +145 -0
  89. tests/unit/test_deprecation.py +104 -0
  90. tests/unit/test_executor.py +172 -0
  91. tests/unit/test_executor_async.py +179 -0
  92. tests/unit/test_export.py +149 -0
  93. tests/unit/test_expressions.py +178 -0
  94. tests/unit/test_feature_brainstorm.py +194 -0
  95. tests/unit/test_format_prompt.py +145 -0
  96. tests/unit/test_generic_report.py +200 -0
  97. tests/unit/test_graph_commands.py +327 -0
  98. tests/unit/test_graph_linter.py +627 -0
  99. tests/unit/test_graph_loader.py +357 -0
  100. tests/unit/test_graph_schema.py +193 -0
  101. tests/unit/test_inline_schema.py +151 -0
  102. tests/unit/test_interrupt_node.py +182 -0
  103. tests/unit/test_issues.py +164 -0
  104. tests/unit/test_jinja2_prompts.py +85 -0
  105. tests/unit/test_json_extract.py +134 -0
  106. tests/unit/test_langsmith.py +600 -0
  107. tests/unit/test_langsmith_tools.py +204 -0
  108. tests/unit/test_llm_factory.py +109 -0
  109. tests/unit/test_llm_factory_async.py +118 -0
  110. tests/unit/test_loops.py +403 -0
  111. tests/unit/test_map_node.py +144 -0
  112. tests/unit/test_no_backward_compat.py +56 -0
  113. tests/unit/test_node_factory.py +348 -0
  114. tests/unit/test_passthrough_node.py +126 -0
  115. tests/unit/test_prompts.py +324 -0
  116. tests/unit/test_python_nodes.py +198 -0
  117. tests/unit/test_reliability.py +298 -0
  118. tests/unit/test_result_export.py +234 -0
  119. tests/unit/test_router.py +296 -0
  120. tests/unit/test_sanitize.py +99 -0
  121. tests/unit/test_schema_loader.py +295 -0
  122. tests/unit/test_shell_tools.py +229 -0
  123. tests/unit/test_state_builder.py +331 -0
  124. tests/unit/test_state_builder_map.py +104 -0
  125. tests/unit/test_state_config.py +197 -0
  126. tests/unit/test_streaming.py +307 -0
  127. tests/unit/test_subgraph.py +596 -0
  128. tests/unit/test_template.py +190 -0
  129. tests/unit/test_tool_call_integration.py +164 -0
  130. tests/unit/test_tool_call_node.py +178 -0
  131. tests/unit/test_tool_nodes.py +129 -0
  132. tests/unit/test_websearch.py +234 -0
  133. yamlgraph/__init__.py +35 -0
  134. yamlgraph/builder.py +110 -0
  135. yamlgraph/cli/__init__.py +159 -0
  136. yamlgraph/cli/__main__.py +6 -0
  137. yamlgraph/cli/commands.py +231 -0
  138. yamlgraph/cli/deprecation.py +92 -0
  139. yamlgraph/cli/graph_commands.py +541 -0
  140. yamlgraph/cli/validators.py +37 -0
  141. yamlgraph/config.py +67 -0
  142. yamlgraph/constants.py +70 -0
  143. yamlgraph/error_handlers.py +227 -0
  144. yamlgraph/executor.py +290 -0
  145. yamlgraph/executor_async.py +288 -0
  146. yamlgraph/graph_loader.py +451 -0
  147. yamlgraph/map_compiler.py +150 -0
  148. yamlgraph/models/__init__.py +36 -0
  149. yamlgraph/models/graph_schema.py +181 -0
  150. yamlgraph/models/schemas.py +124 -0
  151. yamlgraph/models/state_builder.py +236 -0
  152. yamlgraph/node_factory.py +768 -0
  153. yamlgraph/routing.py +87 -0
  154. yamlgraph/schema_loader.py +240 -0
  155. yamlgraph/storage/__init__.py +20 -0
  156. yamlgraph/storage/checkpointer.py +72 -0
  157. yamlgraph/storage/checkpointer_factory.py +123 -0
  158. yamlgraph/storage/database.py +320 -0
  159. yamlgraph/storage/export.py +269 -0
  160. yamlgraph/tools/__init__.py +1 -0
  161. yamlgraph/tools/agent.py +320 -0
  162. yamlgraph/tools/graph_linter.py +388 -0
  163. yamlgraph/tools/langsmith_tools.py +125 -0
  164. yamlgraph/tools/nodes.py +126 -0
  165. yamlgraph/tools/python_tool.py +179 -0
  166. yamlgraph/tools/shell.py +205 -0
  167. yamlgraph/tools/websearch.py +242 -0
  168. yamlgraph/utils/__init__.py +48 -0
  169. yamlgraph/utils/conditions.py +157 -0
  170. yamlgraph/utils/expressions.py +245 -0
  171. yamlgraph/utils/json_extract.py +104 -0
  172. yamlgraph/utils/langsmith.py +416 -0
  173. yamlgraph/utils/llm_factory.py +118 -0
  174. yamlgraph/utils/llm_factory_async.py +105 -0
  175. yamlgraph/utils/logging.py +104 -0
  176. yamlgraph/utils/prompts.py +171 -0
  177. yamlgraph/utils/sanitize.py +98 -0
  178. yamlgraph/utils/template.py +102 -0
  179. yamlgraph/utils/validators.py +181 -0
  180. yamlgraph-0.3.9.dist-info/METADATA +1105 -0
  181. yamlgraph-0.3.9.dist-info/RECORD +185 -0
  182. yamlgraph-0.3.9.dist-info/WHEEL +5 -0
  183. yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
  184. yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
  185. 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,5 @@
1
+ """NPC example nodes for image generation."""
2
+
3
+ from .image_node import generate_scene_image_node
4
+
5
+ __all__ = ["generate_scene_image_node"]
@@ -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
+ }