hexdag-forge 0.2.0.dev3__tar.gz → 0.2.0.dev4__tar.gz

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 (71) hide show
  1. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/PKG-INFO +1 -1
  2. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/assembler.py +31 -14
  3. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/django_generator.py +37 -2
  4. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/pipeline_generator.py +4 -0
  5. hexdag_forge-0.2.0.dev4/hexdag_forge/generator/template_engine.py +209 -0
  6. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/mcp/server.py +1 -1
  7. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/chat_pipeline.yaml.j2 +33 -0
  8. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_base.html.j2 +149 -0
  9. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_chat_agent.py.j2 +97 -0
  10. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_dashboard.html.j2 +47 -0
  11. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_entity_detail_partial.html.j2 +43 -0
  12. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_form.html.j2 +5 -5
  13. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_entity_form_modal.html.j2 +13 -0
  14. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_list.html.j2 +13 -7
  15. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_entity_list_partial.html.j2 +15 -0
  16. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_settings.py.j2 +6 -0
  17. hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_templatetags.py.j2 +43 -0
  18. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_urls.py.j2 +3 -2
  19. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_views.py.j2 +58 -3
  20. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/pyproject.toml +1 -1
  21. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/test_assembler.py +158 -2
  22. hexdag_forge-0.2.0.dev3/hexdag_forge/generator/template_engine.py +0 -64
  23. hexdag_forge-0.2.0.dev3/hexdag_forge/templates/django_base.html.j2 +0 -50
  24. hexdag_forge-0.2.0.dev3/hexdag_forge/templates/django_entity_detail_partial.html.j2 +0 -29
  25. hexdag_forge-0.2.0.dev3/hexdag_forge/templates/django_entity_list_partial.html.j2 +0 -11
  26. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/.gitignore +0 -0
  27. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/LICENSE +0 -0
  28. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/README.md +0 -0
  29. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/__init__.py +0 -0
  30. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/cli.py +0 -0
  31. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/__init__.py +0 -0
  32. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/business_profile.py +0 -0
  33. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/enums.py +0 -0
  34. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/generated_system.py +0 -0
  35. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/profile_diff.py +0 -0
  36. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/__init__.py +0 -0
  37. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/diff.py +0 -0
  38. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/entity_generator.py +0 -0
  39. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/field_mapping.py +0 -0
  40. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/migration_generator.py +0 -0
  41. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/service_generator.py +0 -0
  42. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/system_generator.py +0 -0
  43. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/mcp/__init__.py +0 -0
  44. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/mcp/__main__.py +0 -0
  45. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge.md +0 -0
  46. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge_entity.md +0 -0
  47. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge_implement.md +0 -0
  48. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge_test.md +0 -0
  49. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_admin.py.j2 +0 -0
  50. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_apps.py.j2 +0 -0
  51. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_chat.html.j2 +0 -0
  52. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_chat_conversations.html.j2 +0 -0
  53. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_chat_messages_partial.html.j2 +0 -0
  54. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_confirm_delete.html.j2 +0 -0
  55. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_detail.html.j2 +0 -0
  56. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_forms.py.j2 +0 -0
  57. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_logged_out.html.j2 +0 -0
  58. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_login.html.j2 +0 -0
  59. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_manage.py.j2 +0 -0
  60. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_migration.py.j2 +0 -0
  61. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_model.py.j2 +0 -0
  62. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_register.html.j2 +0 -0
  63. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/pipeline.yaml.j2 +0 -0
  64. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/service.py.j2 +0 -0
  65. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/state_machines.py.j2 +0 -0
  66. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/system.yaml.j2 +0 -0
  67. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/__init__.py +0 -0
  68. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_domain.py +0 -0
  69. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/__init__.py +0 -0
  70. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/test_diff.py +0 -0
  71. {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/test_migration_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexdag-forge
3
- Version: 0.2.0.dev3
3
+ Version: 0.2.0.dev4
4
4
  Summary: Generate operational systems from prompts — code generator powered by hexDAG
5
5
  Project-URL: Homepage, https://hexdag.ai
6
6
  Project-URL: Repository, https://github.com/omniviser/hexdag
@@ -18,12 +18,19 @@ from hexdag_forge.generator.service_generator import generate_services
18
18
  from hexdag_forge.generator.system_generator import generate_system
19
19
 
20
20
 
21
- def assemble(profile: BusinessProfile, output_dir: Path) -> GeneratedSystem:
21
+ def assemble(
22
+ profile: BusinessProfile,
23
+ output_dir: Path,
24
+ *,
25
+ skip_migration: bool = False,
26
+ ) -> GeneratedSystem:
22
27
  """Generate all files from a BusinessProfile and write to output_dir.
23
28
 
24
29
  Args:
25
30
  profile: The business profile to generate from.
26
31
  output_dir: Directory to write generated files to.
32
+ skip_migration: If True, skip migration generation (used when the
33
+ caller has already generated the migration, e.g. ``forge_migrate``).
27
34
 
28
35
  Returns:
29
36
  GeneratedSystem with metadata about what was generated.
@@ -67,19 +74,29 @@ def assemble(profile: BusinessProfile, output_dir: Path) -> GeneratedSystem:
67
74
  migrations_dir.mkdir(exist_ok=True)
68
75
  (migrations_dir / "__init__.py").write_text("")
69
76
 
70
- from hexdag_forge.generator.migration_generator import generate_migration
71
-
72
- empty_profile = BusinessProfile(
73
- business_name=profile.business_name,
74
- business_type=profile.business_type,
75
- description=profile.description,
76
- entities=[],
77
- app_label=profile.app_label,
78
- )
79
- result = generate_migration(empty_profile, profile, migrations_dir)
80
- if result is not None:
81
- filename, migration_content = result
82
- (migrations_dir / filename).write_text(migration_content)
77
+ if not skip_migration:
78
+ from hexdag_forge.generator.migration_generator import generate_migration
79
+
80
+ # Use existing profile.json (from a previous run) as the old state so
81
+ # that diff_profiles produces incremental operations (AddField, etc.)
82
+ # instead of CreateModel for every entity. On the first run the file
83
+ # does not exist yet, so we fall back to an empty profile.
84
+ profile_path = output_dir / "profile.json"
85
+ if profile_path.exists():
86
+ old_profile = BusinessProfile.model_validate_json(profile_path.read_text())
87
+ else:
88
+ old_profile = BusinessProfile(
89
+ business_name=profile.business_name,
90
+ business_type=profile.business_type,
91
+ description=profile.description,
92
+ entities=[],
93
+ app_label=profile.app_label,
94
+ )
95
+
96
+ result = generate_migration(old_profile, profile, migrations_dir)
97
+ if result is not None:
98
+ filename, migration_content = result
99
+ (migrations_dir / filename).write_text(migration_content)
83
100
 
84
101
  # Write profile.json for refinement
85
102
  (output_dir / "profile.json").write_text(profile.model_dump_json(indent=2))
@@ -4,7 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from hexdag_forge.generator.template_engine import render_html_template, render_template
7
+ from hexdag_forge.generator.template_engine import (
8
+ build_badge_map,
9
+ build_fk_fields,
10
+ build_list_columns,
11
+ render_html_template,
12
+ render_template,
13
+ )
8
14
 
9
15
  if TYPE_CHECKING:
10
16
  from hexdag_forge.domain.business_profile import BusinessProfile
@@ -17,8 +23,13 @@ def generate_django(profile: BusinessProfile) -> dict[str, str]:
17
23
  Dict of relative path -> file content.
18
24
  """
19
25
  app = profile.app_label
26
+ badge_colors = build_badge_map(profile)
20
27
  files: dict[str, str] = {}
21
28
 
29
+ # Pre-compute per-entity context
30
+ list_columns_map = {e.name: build_list_columns(e) for e in profile.entities}
31
+ fk_fields_map = {e.name: build_fk_fields(e) for e in profile.entities}
32
+
22
33
  # ── Python files (standard Jinja2 delimiters) ──────────
23
34
  files[f"{app}/models.py"] = render_template("django_model.py.j2", profile=profile)
24
35
  files[f"{app}/apps.py"] = render_template("django_apps.py.j2", profile=profile)
@@ -29,10 +40,24 @@ def generate_django(profile: BusinessProfile) -> dict[str, str]:
29
40
  files["state_machines.py"] = render_template("state_machines.py.j2", profile=profile)
30
41
  files["views.py"] = render_template("django_views.py.j2", profile=profile)
31
42
  files["forms.py"] = render_template("django_forms.py.j2", profile=profile)
43
+ files["chat_agent.py"] = render_template("django_chat_agent.py.j2", profile=profile)
44
+
45
+ # Templatetags
46
+ files[f"{app}/templatetags/__init__.py"] = ""
47
+ files[f"{app}/templatetags/{app}_tags.py"] = render_template(
48
+ "django_templatetags.py.j2", profile=profile, badge_colors=badge_colors
49
+ )
32
50
 
33
51
  # ── HTML templates (alt delimiters — outputs Django template syntax) ──
34
52
  files["templates/base.html"] = render_html_template("django_base.html.j2", profile=profile)
35
53
 
54
+ # Dashboard
55
+ files["templates/dashboard/index.html"] = render_html_template(
56
+ "django_dashboard.html.j2",
57
+ profile=profile,
58
+ list_columns=list_columns_map,
59
+ )
60
+
36
61
  # Auth
37
62
  files["templates/registration/login.html"] = render_html_template(
38
63
  "django_login.html.j2", profile=profile
@@ -47,7 +72,14 @@ def generate_django(profile: BusinessProfile) -> dict[str, str]:
47
72
  # Per-entity CRUD templates
48
73
  for entity in profile.entities:
49
74
  prefix = f"templates/{entity.name}"
50
- ctx = {"entity": entity, "profile": profile}
75
+ list_cols = list_columns_map[entity.name]
76
+ fk_flds = fk_fields_map[entity.name]
77
+ ctx = {
78
+ "entity": entity,
79
+ "profile": profile,
80
+ "list_columns": list_cols,
81
+ "fk_fields": fk_flds,
82
+ }
51
83
  files[f"{prefix}/list.html"] = render_html_template("django_entity_list.html.j2", **ctx)
52
84
  files[f"{prefix}/_list_rows.html"] = render_html_template(
53
85
  "django_entity_list_partial.html.j2", **ctx
@@ -57,6 +89,9 @@ def generate_django(profile: BusinessProfile) -> dict[str, str]:
57
89
  "django_entity_detail_partial.html.j2", **ctx
58
90
  )
59
91
  files[f"{prefix}/form.html"] = render_html_template("django_entity_form.html.j2", **ctx)
92
+ files[f"{prefix}/_form_modal.html"] = render_html_template(
93
+ "django_entity_form_modal.html.j2", **ctx
94
+ )
60
95
  files[f"{prefix}/confirm_delete.html"] = render_html_template(
61
96
  "django_entity_confirm_delete.html.j2", **ctx
62
97
  )
@@ -21,4 +21,8 @@ def generate_pipelines(profile: BusinessProfile) -> dict[str, str]:
21
21
  filename = f"{entity.name}_lifecycle.yaml"
22
22
  content = render_template("pipeline.yaml.j2", entity=entity)
23
23
  pipelines[filename] = content
24
+
25
+ # Chat agent pipeline
26
+ pipelines["chat_agent.yaml"] = render_template("chat_pipeline.yaml.j2", profile=profile)
27
+
24
28
  return pipelines
@@ -0,0 +1,209 @@
1
+ """Jinja2 template rendering engine for code generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path # noqa: TC003
6
+ from typing import TYPE_CHECKING
7
+
8
+ from jinja2 import Environment, FileSystemLoader
9
+
10
+ if TYPE_CHECKING:
11
+ from hexdag_forge.domain.business_profile import BusinessProfile, EntitySpec
12
+
13
+ _TEMPLATE_DIR = Path(__file__).parent.parent / "templates"
14
+
15
+
16
+ # ── Jinja2 filters ──────────────────────────────────────────
17
+
18
+
19
+ def _to_pascal(value: str) -> str:
20
+ """Convert snake_case to PascalCase: ``freight_brokerage`` → ``FreightBrokerage``."""
21
+ return "".join(word.capitalize() for word in value.split("_"))
22
+
23
+
24
+ def _humanize(value: str) -> str:
25
+ """Convert snake_case to human-readable: ``company_name`` → ``Company Name``."""
26
+ return value.replace("_", " ").title()
27
+
28
+
29
+ def _status_color(state: str) -> str:
30
+ """Map a state name to a CSS badge color class using naming heuristics."""
31
+ s = state.upper()
32
+ # Exact matches
33
+ _GREEN = {
34
+ "COMPLETED",
35
+ "DELIVERED",
36
+ "DONE",
37
+ "APPROVED",
38
+ "ACTIVE",
39
+ "PAID",
40
+ "RESOLVED",
41
+ "AVAILABLE",
42
+ "PICKED_UP",
43
+ }
44
+ _RED = {
45
+ "CANCELLED",
46
+ "REJECTED",
47
+ "FAILED",
48
+ "SUSPENDED",
49
+ "BLOCKED",
50
+ "EXPIRED",
51
+ "DELETED",
52
+ "TERMINATED",
53
+ "VOID",
54
+ "RETIRED",
55
+ "CLOSED",
56
+ }
57
+ _BLUE = {
58
+ "DISPATCHED",
59
+ "SENT",
60
+ "ONBOARDING",
61
+ "CONFIRMED",
62
+ "ASSIGNED",
63
+ }
64
+ _PURPLE = {
65
+ "IN_TRANSIT",
66
+ "IN_PROGRESS",
67
+ "PROCESSING",
68
+ "ON_TRIP",
69
+ "IN_USE",
70
+ "COVERED",
71
+ }
72
+ _YELLOW = {
73
+ "SUSPENDED",
74
+ "MAINTENANCE",
75
+ "INVOICED",
76
+ "OVERDUE",
77
+ "OFF_DUTY",
78
+ }
79
+ _GRAY = {
80
+ "DRAFT",
81
+ "PENDING",
82
+ "NEW",
83
+ "POSTED",
84
+ "PROSPECT",
85
+ "CREATED",
86
+ "QUEUED",
87
+ }
88
+
89
+ if s in _GREEN:
90
+ return "badge-green"
91
+ if s in _RED:
92
+ return "badge-red"
93
+ if s in _PURPLE:
94
+ return "badge-purple"
95
+ if s in _YELLOW:
96
+ return "badge-yellow"
97
+ if s in _BLUE:
98
+ return "badge-blue"
99
+ if s in _GRAY:
100
+ return "badge-gray"
101
+
102
+ # Keyword heuristic fallback
103
+ if any(kw in s for kw in ("COMPLET", "DELIVER", "DONE", "APPROV", "ACTIV", "AVAIL", "PAID")):
104
+ return "badge-green"
105
+ if any(kw in s for kw in ("CANCEL", "REJECT", "FAIL", "TERMINAT", "VOID", "RETIR", "CLOSE")):
106
+ return "badge-red"
107
+ if any(kw in s for kw in ("TRANSIT", "TRIP", "PROGRESS", "PROCESS", "USE")):
108
+ return "badge-purple"
109
+ if any(kw in s for kw in ("SUSPEND", "MAINT", "OVERDUE")):
110
+ return "badge-yellow"
111
+ if any(kw in s for kw in ("DISPATCH", "SENT", "ONBOARD", "REVIEW")):
112
+ return "badge-blue"
113
+ return "badge-gray"
114
+
115
+
116
+ def _transition_color(state: str) -> str:
117
+ """Map a target state to a button CSS class for transition buttons."""
118
+ color = _status_color(state)
119
+ return {
120
+ "badge-green": "btn-success",
121
+ "badge-red": "btn-danger",
122
+ "badge-purple": "btn-primary",
123
+ "badge-blue": "btn-primary",
124
+ "badge-yellow": "btn-warning",
125
+ "badge-gray": "btn-outline",
126
+ }.get(color, "btn-outline")
127
+
128
+
129
+ # ── Context builders ────────────────────────────────────────
130
+
131
+
132
+ def build_badge_map(profile: BusinessProfile) -> dict[str, str]:
133
+ """Build a {state_name: badge_class} map for all states across all entities."""
134
+ result: dict[str, str] = {}
135
+ for entity in profile.entities:
136
+ for state in entity.states:
137
+ result[state] = _status_color(state)
138
+ return result
139
+
140
+
141
+ def build_list_columns(entity: EntitySpec) -> list[object]:
142
+ """Select fields suitable for list table display (skip text/json, cap at 5)."""
143
+ return [f for f in entity.fields if f.type not in ("text", "json")][:5]
144
+
145
+
146
+ def build_fk_fields(entity: EntitySpec) -> list[object]:
147
+ """Return belongs_to relationship specs for select_related."""
148
+ return [r for r in entity.relationships if r.kind == "belongs_to"]
149
+
150
+
151
+ # ── Environments ────────────────────────────────────────────
152
+
153
+
154
+ def _register_filters(env: Environment) -> None:
155
+ """Register all custom filters on a Jinja2 environment."""
156
+ env.filters["to_pascal"] = _to_pascal
157
+ env.filters["humanize"] = _humanize
158
+ env.filters["status_color"] = _status_color
159
+ env.filters["transition_color"] = _transition_color
160
+
161
+
162
+ def create_env() -> Environment:
163
+ """Create a Jinja2 environment configured for Python code generation."""
164
+ env = Environment( # nosec B701 — code generation templates, not HTML
165
+ loader=FileSystemLoader(str(_TEMPLATE_DIR)),
166
+ keep_trailing_newline=True,
167
+ trim_blocks=True,
168
+ lstrip_blocks=True,
169
+ autoescape=False,
170
+ )
171
+ _register_filters(env)
172
+ return env
173
+
174
+
175
+ def create_html_env() -> Environment:
176
+ """Create a Jinja2 environment for templates that OUTPUT Django template syntax.
177
+
178
+ Uses alternate delimiters to avoid collision with Django's ``{% %}`` and ``{{ }}``.
179
+ Jinja2 generation uses ``[% %]`` and ``[[ ]]``, Django tags pass through untouched.
180
+ """
181
+ env = Environment( # nosec B701 — code generation templates, not HTML
182
+ loader=FileSystemLoader(str(_TEMPLATE_DIR)),
183
+ keep_trailing_newline=True,
184
+ trim_blocks=True,
185
+ lstrip_blocks=True,
186
+ autoescape=False,
187
+ block_start_string="[%",
188
+ block_end_string="%]",
189
+ variable_start_string="[[",
190
+ variable_end_string="]]",
191
+ comment_start_string="[#",
192
+ comment_end_string="#]",
193
+ )
194
+ _register_filters(env)
195
+ return env
196
+
197
+
198
+ def render_template(template_name: str, **context: object) -> str:
199
+ """Render a Jinja2 template with the given context (standard delimiters)."""
200
+ env = create_env()
201
+ template = env.get_template(template_name)
202
+ return template.render(**context)
203
+
204
+
205
+ def render_html_template(template_name: str, **context: object) -> str:
206
+ """Render a Jinja2 template that outputs Django template syntax (alt delimiters)."""
207
+ env = create_html_env()
208
+ template = env.get_template(template_name)
209
+ return template.render(**context)
@@ -117,7 +117,7 @@ async def forge_migrate(new_profile_json: str, output_dir: str = "./generated")
117
117
  migration_info = {"migration": filename, "operations": content.count("migrations.")}
118
118
 
119
119
  # Regenerate all files with new profile
120
- gen_result = assemble(new_profile, output)
120
+ gen_result = assemble(new_profile, output, skip_migration=True)
121
121
 
122
122
  return json.dumps(
123
123
  {
@@ -0,0 +1,33 @@
1
+ apiVersion: hexdag/v1
2
+ kind: Pipeline
3
+ metadata:
4
+ name: chat_agent
5
+ description: "Chat agent for {{ profile.business_name }} — routes user messages to entity services"
6
+ spec:
7
+ nodes:
8
+ - kind: agent_node
9
+ metadata:
10
+ name: agent
11
+ spec:
12
+ main_prompt: |
13
+ You are an operations assistant for {{ profile.business_name }}.
14
+ You help users manage the following entities:
15
+ {% for entity in profile.entities %}
16
+
17
+ ## {{ entity.display_name }}
18
+ Fields: {{ entity.fields | map(attribute='name') | join(', ') }}
19
+ States: {{ entity.states | join(' → ') }}
20
+ Valid transitions:
21
+ {% for from_state, to_states in entity.transitions.items() %}
22
+ {{ from_state }} → {{ to_states | join(', ') }}
23
+ {% endfor %}
24
+ {% endfor %}
25
+
26
+ Use the available tools to look up, create, update, list, and transition entities.
27
+ Always confirm destructive actions before executing them.
28
+ When listing results, format them clearly.
29
+
30
+ User message: {% raw %}{{message}}{% endraw %}
31
+ config:
32
+ max_steps: 10
33
+ dependencies: []
@@ -0,0 +1,149 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{% block title %}[[ profile.business_name ]]{% endblock %}</title>
7
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
8
+ <style>
9
+ /* ── Reset & Base ───────────────────────────── */
10
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #1f2937; background: #f9fafb; display: flex; min-height: 100vh; font-size: 0.9rem; line-height: 1.5; }
12
+ a { color: #2563eb; text-decoration: none; }
13
+ a:hover { text-decoration: underline; }
14
+ h1, h2, h3 { font-weight: 600; color: #111827; }
15
+ h2 { font-size: 1.4rem; }
16
+
17
+ /* ── Sidebar ────────────────────────────────── */
18
+ .sidebar { width: 230px; background: #111827; color: #d1d5db; padding: 1.25rem 0; display: flex; flex-direction: column; flex-shrink: 0; }
19
+ .sidebar-brand { padding: 0 1.25rem 1rem; font-size: 1.05rem; font-weight: 700; color: #fff; border-bottom: 1px solid #374151; margin-bottom: 0.75rem; }
20
+ .sidebar-section { padding: 0.5rem 1.25rem 0.25rem; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.08em; color: #6b7280; }
21
+ .sidebar ul { list-style: none; }
22
+ .sidebar li a { display: block; padding: 0.4rem 1.25rem; color: #d1d5db; font-size: 0.85rem; transition: background 0.15s; }
23
+ .sidebar li a:hover { background: #1f2937; color: #fff; text-decoration: none; }
24
+ .sidebar li a.active { background: #2563eb; color: #fff; }
25
+
26
+ /* ── Main ───────────────────────────────────── */
27
+ .main { flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
28
+ .topbar { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1.5rem; background: #fff; border-bottom: 1px solid #e5e7eb; }
29
+ .topbar .user-info { font-size: 0.85rem; color: #6b7280; }
30
+ .topbar .user-info a { color: #6b7280; margin-left: 0.5rem; }
31
+ .content { padding: 1.5rem; max-width: 1100px; }
32
+
33
+ /* ── Badges ─────────────────────────────────── */
34
+ .badge { display: inline-block; padding: 0.15rem 0.55rem; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; white-space: nowrap; }
35
+ .badge-blue { background: #dbeafe; color: #1e40af; }
36
+ .badge-green { background: #dcfce7; color: #166534; }
37
+ .badge-yellow { background: #fef3c7; color: #92400e; }
38
+ .badge-red { background: #fee2e2; color: #991b1b; }
39
+ .badge-gray { background: #f3f4f6; color: #374151; }
40
+ .badge-purple { background: #f3e8ff; color: #6b21a8; }
41
+ .badge-indigo { background: #e0e7ff; color: #3730a3; }
42
+
43
+ /* ── Buttons ────────────────────────────────── */
44
+ .btn { display: inline-block; padding: 0.4rem 0.9rem; border-radius: 6px; font-size: 0.8rem; font-weight: 500; cursor: pointer; border: 1px solid transparent; text-decoration: none; transition: background 0.15s; }
45
+ .btn-primary { background: #2563eb; color: #fff; }
46
+ .btn-primary:hover { background: #1d4ed8; text-decoration: none; }
47
+ .btn-success { background: #16a34a; color: #fff; }
48
+ .btn-success:hover { background: #15803d; text-decoration: none; }
49
+ .btn-danger { background: #dc2626; color: #fff; }
50
+ .btn-danger:hover { background: #b91c1c; text-decoration: none; }
51
+ .btn-warning { background: #d97706; color: #fff; }
52
+ .btn-warning:hover { background: #b45309; text-decoration: none; }
53
+ .btn-outline { background: transparent; color: #374151; border-color: #d1d5db; }
54
+ .btn-outline:hover { background: #f3f4f6; text-decoration: none; }
55
+ .btn-sm { padding: 0.25rem 0.6rem; font-size: 0.72rem; }
56
+
57
+ /* ── Tables ─────────────────────────────────── */
58
+ table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
59
+ thead { background: #f9fafb; }
60
+ th { text-align: left; padding: 0.6rem 0.75rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; font-weight: 600; border-bottom: 1px solid #e5e7eb; }
61
+ td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #f3f4f6; font-size: 0.85rem; }
62
+ tr.clickable { cursor: pointer; }
63
+ tr.clickable:hover { background: #f0f9ff; }
64
+
65
+ /* ── Filter pills ───────────────────────────── */
66
+ .filter-pills { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 1rem; }
67
+ .filter-pills a { padding: 0.25rem 0.7rem; border-radius: 9999px; font-size: 0.78rem; border: 1px solid #d1d5db; color: #374151; }
68
+ .filter-pills a:hover { background: #f3f4f6; text-decoration: none; }
69
+ .filter-pills a.active { background: #2563eb; color: #fff; border-color: #2563eb; }
70
+
71
+ /* ── Cards ──────────────────────────────────── */
72
+ .card { background: #fff; border-radius: 8px; padding: 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
73
+ .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
74
+ .stat-card { background: #fff; border-radius: 8px; padding: 1rem 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
75
+ .stat-card .label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin-bottom: 0.25rem; }
76
+ .stat-card .value { font-size: 1.5rem; font-weight: 700; color: #111827; }
77
+
78
+ /* ── Detail grid ────────────────────────────── */
79
+ .detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 2rem; }
80
+ .detail-item { padding: 0.6rem 0; border-bottom: 1px solid #f3f4f6; }
81
+ .detail-item .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin-bottom: 0.15rem; }
82
+ .detail-item .value { font-size: 0.9rem; color: #1f2937; }
83
+
84
+ /* ── Forms ──────────────────────────────────── */
85
+ .form-card { background: #fff; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); max-width: 600px; }
86
+ .form-card p { margin-bottom: 0.75rem; }
87
+ .form-card label { display: block; font-size: 0.78rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem; }
88
+ .form-card input, .form-card select, .form-card textarea { width: 100%; padding: 0.45rem 0.6rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; }
89
+ .form-card input:focus, .form-card select:focus, .form-card textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.15); }
90
+
91
+ /* ── Modal ──────────────────────────────────── */
92
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 100; }
93
+ .modal-content { background: #fff; border-radius: 12px; padding: 1.5rem; width: 90%; max-width: 550px; max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.2); }
94
+ .modal-content h2 { margin-bottom: 1rem; }
95
+
96
+ /* ── Misc ───────────────────────────────────── */
97
+ .toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
98
+ .action-row { display: flex; gap: 0.5rem; margin-top: 1rem; }
99
+ hr { border: none; border-top: 1px solid #e5e7eb; margin: 1rem 0; }
100
+ .empty-state { text-align: center; padding: 2rem; color: #9ca3af; }
101
+ </style>
102
+ </head>
103
+ <body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
104
+ <nav class="sidebar">
105
+ <div class="sidebar-brand">[[ profile.business_name ]]</div>
106
+ <ul>
107
+ <li><a href="/">&#9632; Dashboard</a></li>
108
+ </ul>
109
+ [% if profile.modules %]
110
+ [% for module in profile.modules %]
111
+ <div class="sidebar-section">[[ module | humanize ]]</div>
112
+ <ul>
113
+ [% for entity in profile.entities %]
114
+ <li><a href="/[[ entity.name ]]/">[[ entity.display_name ]]</a></li>
115
+ [% endfor %]
116
+ </ul>
117
+ [% endfor %]
118
+ [% else %]
119
+ <div class="sidebar-section">Entities</div>
120
+ <ul>
121
+ [% for entity in profile.entities %]
122
+ <li><a href="/[[ entity.name ]]/">[[ entity.display_name ]]</a></li>
123
+ [% endfor %]
124
+ </ul>
125
+ [% endif %]
126
+ <div class="sidebar-section" style="margin-top: auto;">Tools</div>
127
+ <ul>
128
+ <li><a href="/chat/">&#9993; Chat</a></li>
129
+ <li><a href="/admin/">&#9881; Admin</a></li>
130
+ </ul>
131
+ </nav>
132
+ <div class="main">
133
+ <div class="topbar">
134
+ <div>{% block heading %}{% endblock %}</div>
135
+ <div class="user-info">
136
+ {% if user.is_authenticated %}
137
+ {{ user.username }} <a href="/logout/">Logout</a>
138
+ {% else %}
139
+ <a href="/login/">Login</a>
140
+ {% endif %}
141
+ </div>
142
+ </div>
143
+ <div class="content">
144
+ {% block content %}{% endblock %}
145
+ </div>
146
+ </div>
147
+ <div id="modal-container"></div>
148
+ </body>
149
+ </html>
@@ -0,0 +1,97 @@
1
+ """Chat agent for {{ profile.business_name }}.
2
+
3
+ Runs a hexDAG ReActAgentNode pipeline that routes user messages
4
+ to entity service tools. Requires an LLM API key.
5
+
6
+ Auto-generated by hexdag-forge.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import os
13
+ from pathlib import Path
14
+
15
+ from hexdag.api.execution import execute
16
+
17
+
18
+ _PIPELINE_PATH = Path(__file__).parent / "pipelines" / "chat_agent.yaml"
19
+ _pipeline_cache: str | None = None
20
+
21
+
22
+ def _load_pipeline() -> str:
23
+ """Load the chat pipeline YAML (cached after first read)."""
24
+ global _pipeline_cache # noqa: PLW0603
25
+ if _pipeline_cache is None:
26
+ _pipeline_cache = _PIPELINE_PATH.read_text()
27
+ return _pipeline_cache
28
+
29
+
30
+ def _create_llm():
31
+ """Create an LLM adapter from environment configuration.
32
+
33
+ Checks for API keys in order: OpenAI → Anthropic → Google Gemini.
34
+ Set HEXDAG_CHAT_MODEL to override the default model for each provider.
35
+ """
36
+ model = os.environ.get("HEXDAG_CHAT_MODEL", "")
37
+
38
+ # OpenAI
39
+ if os.environ.get("OPENAI_API_KEY"):
40
+ from hexdag.stdlib.adapters.openai import OpenAIAdapter
41
+
42
+ return OpenAIAdapter(model=model or "gpt-4o-mini", temperature=0.7)
43
+
44
+ # Anthropic
45
+ if os.environ.get("ANTHROPIC_API_KEY"):
46
+ from hexdag.stdlib.adapters.anthropic import AnthropicAdapter
47
+
48
+ return AnthropicAdapter(model=model or "claude-sonnet-4-20250514")
49
+
50
+ # Google Gemini (via Vertex AI plugin)
51
+ if os.environ.get("GOOGLE_API_KEY") or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
52
+ from hexdag_plugins.google.adapters.vertex import VertexAIAdapter
53
+
54
+ return VertexAIAdapter(model=model or "gemini-2.0-flash")
55
+
56
+ return None
57
+
58
+
59
+ async def process_message(message: str) -> str:
60
+ """Process a chat message through the hexDAG agent pipeline.
61
+
62
+ Args:
63
+ message: The user's message text.
64
+
65
+ Returns:
66
+ The agent's response text.
67
+ """
68
+ llm = _create_llm()
69
+ if llm is None:
70
+ return (
71
+ "No LLM configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, "
72
+ "or GOOGLE_API_KEY environment variable to enable the chat agent."
73
+ )
74
+
75
+ pipeline_yaml = _load_pipeline()
76
+
77
+ result = await execute(
78
+ yaml_content=pipeline_yaml,
79
+ inputs={"message": message},
80
+ ports={"llm": llm},
81
+ timeout=60.0,
82
+ )
83
+
84
+ if not result.get("success"):
85
+ error = result.get("error", "Unknown error")
86
+ return f"Agent error: {error}"
87
+
88
+ # Extract the agent's final output
89
+ final = result.get("final_output", {})
90
+ if isinstance(final, dict):
91
+ return final.get("answer", final.get("content", str(final)))
92
+ return str(final)
93
+
94
+
95
+ def process_message_sync(message: str) -> str:
96
+ """Synchronous wrapper for Django views."""
97
+ return asyncio.run(process_message(message))