Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
exclude: ^src/ai_company/templates/builtins/
- id: check-toml
- id: check-json
- id: check-merge-conflict
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"jinja2==3.1.6",
"pydantic==2.12.5",
"pyyaml==6.0.2",
"structlog==25.5.0",
Expand Down
40 changes: 7 additions & 33 deletions src/ai_company/config/loader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""YAML configuration loader with layered merging and validation."""

import copy
import logging
import os
import re
Expand All @@ -18,6 +17,7 @@
ConfigValidationError,
)
from ai_company.config.schema import RootConfig
from ai_company.config.utils import deep_merge

logger = logging.getLogger(__name__)

Expand All @@ -35,33 +35,6 @@
# ---------------------------------------------------------------------------


def _deep_merge(
base: dict[str, Any],
override: dict[str, Any],
) -> dict[str, Any]:
"""Recursively merge *override* into *base*, returning a new dict.

Nested dicts are merged recursively. Lists, scalars, and all other
types in *override* replace the corresponding value in *base*
entirely. Keys present only in *base* are preserved unchanged in
the result. Neither input dict is mutated.

Args:
base: Base configuration dict.
override: Override values to layer on top.

Returns:
A new merged dict.
"""
result = copy.deepcopy(base)
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge(result[key], value)
else:
result[key] = copy.deepcopy(value)
return result


def _read_config_text(file_path: Path) -> str:
"""Read a configuration file as UTF-8 text.

Expand Down Expand Up @@ -214,10 +187,11 @@ def _build_line_map(yaml_text: str) -> dict[str, tuple[int, int]]:
"""
try:
root = yaml.compose(yaml_text, Loader=yaml.SafeLoader)
except yaml.YAMLError:
except yaml.YAMLError as exc:
logger.warning(
"Failed to compose YAML AST for line mapping; "
"validation errors will lack line/column information",
"validation errors will lack line/column information: %s",
exc,
)
return {}
if root is None or not isinstance(root, yaml.MappingNode):
Expand Down Expand Up @@ -446,12 +420,12 @@ def load_config(
# and line-map construction)
yaml_text = _read_config_text(config_path)
primary = _parse_yaml_string(yaml_text, str(config_path))
merged = _deep_merge(merged, primary)
merged = deep_merge(merged, primary)

# 3. Apply override layers
for override_path in override_paths:
override = _parse_yaml_file(Path(override_path))
merged = _deep_merge(merged, override)
merged = deep_merge(merged, override)

# 4. Substitute environment variables on the fully merged config.
# Use a neutral label so env-var errors aren't misattributed solely
Expand Down Expand Up @@ -491,7 +465,7 @@ def load_config_from_string(
ConfigValidationError: If the merged config fails validation.
"""
data = _parse_yaml_string(yaml_string, source_name)
merged = _deep_merge(default_config_dict(), data)
merged = deep_merge(default_config_dict(), data)
merged = _substitute_env_vars(merged, source_file=source_name)
line_map = _build_line_map(yaml_string)
return _validate_config_dict(
Expand Down
54 changes: 54 additions & 0 deletions src/ai_company/config/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Shared configuration utilities."""

import copy
from typing import Any


def to_float(value: Any, *, field_name: str = "value") -> float:
"""Coerce a value to float with clear error reporting.

Args:
value: Value to convert (str, int, float, etc.).
field_name: Field name for error messages.

Returns:
Float value.

Raises:
ValueError: If *value* cannot be converted to float.
"""
if value is None:
msg = f"Expected numeric value for {field_name}, got None"
raise ValueError(msg)
try:
return float(value)
except (TypeError, ValueError) as exc:
msg = f"Invalid numeric value for {field_name}: {value!r}"
raise ValueError(msg) from exc


def deep_merge(
base: dict[str, Any],
override: dict[str, Any],
) -> dict[str, Any]:
"""Recursively merge *override* into *base*, returning a new dict.

Nested dicts are merged recursively. Lists, scalars, and all other
types in *override* replace the corresponding value in *base*
entirely. Keys present only in *base* are preserved unchanged in
the result. Neither input dict is mutated.

Args:
base: Base configuration dict.
override: Override values to layer on top.

Returns:
A new merged dict.
"""
result = copy.deepcopy(base)
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = copy.deepcopy(value)
return result
64 changes: 64 additions & 0 deletions src/ai_company/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Company templates: built-in presets and custom template loading.

Public API
----------
.. autosummary::
load_template
load_template_file
list_templates
list_builtin_templates
render_template
CompanyTemplate
LoadedTemplate
TemplateInfo
TemplateMetadata
TemplateVariable
TemplateAgentConfig
TemplateDepartmentConfig
TemplateError
TemplateNotFoundError
TemplateRenderError
TemplateValidationError
"""

from ai_company.templates.errors import (
TemplateError,
TemplateNotFoundError,
TemplateRenderError,
TemplateValidationError,
)
from ai_company.templates.loader import (
LoadedTemplate,
TemplateInfo,
list_builtin_templates,
list_templates,
load_template,
load_template_file,
)
from ai_company.templates.renderer import render_template
from ai_company.templates.schema import (
CompanyTemplate,
TemplateAgentConfig,
TemplateDepartmentConfig,
TemplateMetadata,
TemplateVariable,
)

__all__ = [
"CompanyTemplate",
"LoadedTemplate",
"TemplateAgentConfig",
"TemplateDepartmentConfig",
"TemplateError",
"TemplateInfo",
"TemplateMetadata",
"TemplateNotFoundError",
"TemplateRenderError",
"TemplateValidationError",
"TemplateVariable",
"list_builtin_templates",
"list_templates",
"load_template",
"load_template_file",
"render_template",
]
Empty file.
52 changes: 52 additions & 0 deletions src/ai_company/templates/builtins/agency.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
template:
name: "Agency"
description: "Client-focused agency with project management and creative roles"
version: "1.0.0"
tags:
- "agency"
- "client-work"
- "creative"

variables:
- name: "company_name"
description: "Name of your agency"
default: "Creative Agency"
- name: "budget"
description: "Monthly budget in USD"
var_type: "float"
default: 100.0

company:
type: "agency"
budget_monthly: {{ budget | default(100.0) }}
autonomy: 0.5

departments:
- name: "operations"
budget_percent: 30
head_role: "Project Manager"
- name: "engineering"
budget_percent: 40
head_role: "Full-Stack Developer"
- name: "design"
budget_percent: 30
head_role: "UI Designer"

agents:
- role: "Project Manager"
level: "senior"
model: "sonnet"
personality_preset: "visionary_leader"
department: "operations"
- role: "UI Designer"
level: "mid"
model: "sonnet"
department: "design"
- role: "Full-Stack Developer"
level: "senior"
model: "sonnet"
personality_preset: "pragmatic_builder"
department: "engineering"

workflow: "kanban"
communication: "hybrid"
63 changes: 63 additions & 0 deletions src/ai_company/templates/builtins/dev_shop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
template:
name: "Dev Shop"
description: "Software development focused team with QA and DevOps"
version: "1.0.0"
tags:
- "development"
- "engineering"
- "qa"

variables:
- name: "company_name"
description: "Name of your company"
default: "Dev Shop"
- name: "budget"
description: "Monthly budget in USD"
var_type: "float"
default: 75.0

company:
type: "dev_shop"
budget_monthly: {{ budget | default(75.0) }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix Jinja braces spacing on Line 21 to satisfy YAML linting.

budget_monthly: {{ budget | default(75.0) }} is flagged by YAMLlint (braces).

Suggested fix
-    budget_monthly: {{ budget | default(75.0) }}
+    budget_monthly: {{budget | default(75.0)}}

As per coding guidelines **/*.{yaml,yml,json,toml}: Use check-yaml, check-json, and check-toml pre-commit hooks to validate configuration files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
budget_monthly: {{ budget | default(75.0) }}
budget_monthly: {{budget | default(75.0)}}
🧰 Tools
🪛 YAMLlint (1.38.0)

[error] 21-21: too many spaces inside braces

(braces)


[error] 21-21: too many spaces inside braces

(braces)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/templates/builtins/dev_shop.yaml` at line 21, The YAML lint
error is caused by spacing inside the Jinja braces for the budget_monthly value;
update the Jinja expression associated with the budget_monthly key (the template
"{{ budget | default(75.0) }}") to remove the extra space immediately after "{{"
and before "}}" so it conforms to the braces rule (e.g., use the same expression
without those inner-leading/trailing spaces), then re-run the YAML linter to
confirm the file passes.

autonomy: 0.5

departments:
- name: "engineering"
budget_percent: 70
head_role: "Software Architect"
- name: "quality_assurance"
budget_percent: 20
head_role: "QA Lead"
- name: "operations"
budget_percent: 10
head_role: "DevOps/SRE Engineer"

agents:
- role: "Software Architect"
level: "principal"
model: "opus"
personality_preset: "methodical_analyst"
department: "engineering"
- role: "Backend Developer"
level: "senior"
model: "sonnet"
personality_preset: "pragmatic_builder"
department: "engineering"
- role: "Backend Developer"
level: "mid"
model: "haiku"
personality_preset: "eager_learner"
department: "engineering"
- role: "QA Lead"
level: "lead"
model: "sonnet"
personality_preset: "methodical_analyst"
department: "quality_assurance"
- role: "DevOps/SRE Engineer"
level: "mid"
model: "sonnet"
personality_preset: "pragmatic_builder"
department: "operations"

workflow: "agile_kanban"
communication: "hybrid"
Loading