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.
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/PKG-INFO +1 -1
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/assembler.py +31 -14
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/django_generator.py +37 -2
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/pipeline_generator.py +4 -0
- hexdag_forge-0.2.0.dev4/hexdag_forge/generator/template_engine.py +209 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/mcp/server.py +1 -1
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/chat_pipeline.yaml.j2 +33 -0
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_base.html.j2 +149 -0
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_chat_agent.py.j2 +97 -0
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_dashboard.html.j2 +47 -0
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_entity_detail_partial.html.j2 +43 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_form.html.j2 +5 -5
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_entity_form_modal.html.j2 +13 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_list.html.j2 +13 -7
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_entity_list_partial.html.j2 +15 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_settings.py.j2 +6 -0
- hexdag_forge-0.2.0.dev4/hexdag_forge/templates/django_templatetags.py.j2 +43 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_urls.py.j2 +3 -2
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_views.py.j2 +58 -3
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/pyproject.toml +1 -1
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/test_assembler.py +158 -2
- hexdag_forge-0.2.0.dev3/hexdag_forge/generator/template_engine.py +0 -64
- hexdag_forge-0.2.0.dev3/hexdag_forge/templates/django_base.html.j2 +0 -50
- hexdag_forge-0.2.0.dev3/hexdag_forge/templates/django_entity_detail_partial.html.j2 +0 -29
- hexdag_forge-0.2.0.dev3/hexdag_forge/templates/django_entity_list_partial.html.j2 +0 -11
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/.gitignore +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/LICENSE +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/README.md +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/__init__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/cli.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/__init__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/business_profile.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/enums.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/generated_system.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/domain/profile_diff.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/__init__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/diff.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/entity_generator.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/field_mapping.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/migration_generator.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/service_generator.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/system_generator.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/mcp/__init__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/mcp/__main__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge.md +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge_entity.md +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge_implement.md +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/skills/forge_test.md +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_admin.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_apps.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_chat.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_chat_conversations.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_chat_messages_partial.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_confirm_delete.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_entity_detail.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_forms.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_logged_out.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_login.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_manage.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_migration.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_model.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/django_register.html.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/pipeline.yaml.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/service.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/state_machines.py.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/templates/system.yaml.j2 +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/__init__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_domain.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/__init__.py +0 -0
- {hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/tests/test_generator/test_diff.py +0 -0
- {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.
|
|
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(
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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))
|
{hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/django_generator.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
)
|
{hexdag_forge-0.2.0.dev3 → hexdag_forge-0.2.0.dev4}/hexdag_forge/generator/pipeline_generator.py
RENAMED
|
@@ -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="/">■ 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/">✉ Chat</a></li>
|
|
129
|
+
<li><a href="/admin/">⚙ 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))
|