Skip to content

Add post-run learning workflow, YAML agent spec bridge, and tests#70

Open
weilixu wants to merge 1 commit intodevelopfrom
codex/implement-yaml-agent-specification-and-learning-mode
Open

Add post-run learning workflow, YAML agent spec bridge, and tests#70
weilixu wants to merge 1 commit intodevelopfrom
codex/implement-yaml-agent-specification-and-learning-mode

Conversation

@weilixu
Copy link
Copy Markdown
Contributor

@weilixu weilixu commented Mar 25, 2026

Motivation

  • Introduce an MVP post-run learning pipeline to capture reflections and lessons from completed agent runs and persist compact lessons for offline analysis.
  • Provide a small YAML-first bridge so agents can be configured from versioned YAML specs without changing the existing AgentFactory call surface.
  • Wire learning configuration into runtime so LangGraph chat agents can opt into best-effort background reflection/lesson extraction.

Description

  • Add a LearningWorkflowConfig model in automa_ai.config.learning and export it from automa_ai.config.__init__, and wire learning_config through the AgentFactory constructor.
  • Extend GenericLangGraphChatAgent to accept learning_config, change _emit_final_output to return the final item, and add _trigger_learning_workflow which schedules run_learning_workflow via asyncio.create_task and logs background failures in _on_learning_task_done.
  • Implement the learning runtime in automa_ai.learning.workflow with build_review_payload, run_learning_workflow, response normalization, and lesson persistence, and expose them via automa_ai.learning.__init__.
  • Add a YAML-to-factory adapter YamlAgentSpec in automa_ai.config.agent_spec that parses versioned YAML, maps fields to existing AgentFactory kwargs, and constructs agents without changing Python paths.
  • Add documentation AGENT_FACTORY_CONFIG_SURFACE.md, example agent specs for reflection_agent and lesson_agent, and update tool/blackboard/skill wiring to pass learning_config through.

Testing

  • Added and ran unit tests under tests/learning/: test_agent_spec.py, test_learning_mode.py, and test_learning_payload.py using pytest, which validate YAML parsing/mapping, learning scheduling behavior, background failure handling, and payload normalization.
  • All newly added tests completed successfully.

Codex Task

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 208f23d461

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

from typing import Any

from automa_ai.blackboard.store import BlackboardStore
from automa_ai.config.agent_spec import YamlAgentSpec
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Badge Break import cycle in learning workflow module

Importing YamlAgentSpec at module load time creates a circular dependency: langgraph_chatagent -> learning.workflow -> config.agent_spec -> agent_factory -> langgraph_chatagent. Because GenericLangGraphChatAgent is imported before its class definition is executed, this can raise a partially-initialized-module ImportError and prevent agent modules from importing at all when dependencies are installed. Move this import into run_learning_workflow (or otherwise decouple agent_spec from agent_factory) so loading the chat agent does not recursively import itself.

Useful? React with 👍 / 👎.

) -> None:
Path(output_dir).mkdir(parents=True, exist_ok=True)
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
out_path = Path(output_dir) / f"{session_id}-{stamp}.lesson.json"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sanitize session_id before building lesson output path

The lesson filename is composed directly from session_id, so a session value containing path separators (for example ../...) can escape output_dir and write files outside the intended artifact directory. This is a path traversal risk whenever session_id comes from request/user input. Normalize or sanitize session_id to a safe basename (or reject unsafe characters) before constructing out_path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an MVP “post-run learning” pipeline (payload → reflection → lessons persisted to disk) plus a YAML-based agent spec bridge that maps onto the existing AgentFactory constructor surface, with unit tests covering payload normalization, YAML mapping, and background scheduling behavior.

Changes:

  • Introduces LearningWorkflowConfig and wires learning_config through AgentFactory into GenericLangGraphChatAgent.
  • Implements automa_ai.learning.workflow (payload building, workflow execution, lesson persistence) and exports via automa_ai.learning.
  • Adds YamlAgentSpec for versioned YAML parsing/mapping to AgentFactory kwargs, along with example YAML specs and tests.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
automa_ai/learning/workflow.py Implements payload normalization, reflection/lesson invocation, and persistence.
automa_ai/learning/__init__.py Exposes learning workflow API.
automa_ai/config/learning.py Adds the learning workflow config model and defaults.
automa_ai/config/agent_spec.py Adds YAML agent spec parsing and mapping to AgentFactory.
automa_ai/agents/langgraph_chatagent.py Adds background learning scheduling after completion and returns final output item for learning capture.
automa_ai/agents/agent_factory.py Threads learning_config through to LangGraph chat agents.
automa_ai/config/__init__.py Exports LearningWorkflowConfig.
tests/learning/test_* Adds unit tests for YAML spec mapping, scheduling behavior, and payload normalization.
examples/configs/specs/learning/*.yaml Adds example reflection/lesson agent YAML specs.
automa_ai/agents/AGENT_FACTORY_CONFIG_SURFACE.md Documents AgentFactory config surface and YAML wrapper rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

from pathlib import Path
from typing import Any, Literal

import yaml
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

yaml is imported unconditionally, but pyyaml appears to be an optional extra (not a core dependency). This makes importing automa_ai.config.agent_spec (and anything that imports it, like the learning workflow) fail in default installs. Consider either adding pyyaml to core dependencies or moving the import inside from_yaml_* with a clear ImportError instructing how to install the extra.

Suggested change
import yaml
try:
import yaml
except ImportError: # pragma: no cover
class _YAMLImportProxy:
def __getattr__(self, _name: str) -> Any:
raise ImportError(
"PyYAML is required to load YAML agent specifications. "
"Install it with `pip install automa-ai[yaml]` or `pip install pyyaml`."
)
yaml = _YAMLImportProxy() # type: ignore[assignment]

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +138
return {
"kind": "langgraph_messages",
"message_count": len(messages),
"last_message": last_content,
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

When final_response is a dict containing messages, _normalize_final_response replaces the whole dict with a compact summary and drops other fields (e.g., status, errors, validator_failures). Since build_review_payload later tries to extract those fields from the normalized output, this can silently lose failure metadata. Consider preserving/merging whitelisted keys from the original dict into the normalized result.

Suggested change
return {
"kind": "langgraph_messages",
"message_count": len(messages),
"last_message": last_content,
}
summary: dict[str, Any] = {
"kind": "langgraph_messages",
"message_count": len(messages),
"last_message": last_content,
}
# Preserve selected metadata fields from the original response so that
# downstream consumers (e.g., build_review_payload) can still access
# failure/status information after normalization.
for meta_key in ("status", "errors", "validator_failures"):
if meta_key in final_response:
summary[meta_key] = final_response[meta_key]
return summary

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +123
def _extract_response_text(response: Any) -> Any:
if isinstance(response, str):
return response
if isinstance(response, dict):
messages = response.get("messages")
if isinstance(messages, list) and messages:
last = messages[-1]
content = getattr(last, "content", None)
if content is not None:
return content
if isinstance(last, dict):
return last.get("content", str(last))
return response
return str(response)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

_extract_response_text may return the original response dict (when messages is missing/empty). That dict can include non-JSON-serializable objects, and it later gets embedded into _json_prompt/persisted to disk. To keep the workflow best-effort, consider always returning a string (or JSON-safe structure) here, with a safe fallback serialization (e.g., json.dumps(..., default=str) or str(...)).

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +176
Path(output_dir).mkdir(parents=True, exist_ok=True)
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
out_path = Path(output_dir) / f"{session_id}-{stamp}.lesson.json"
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

session_id is interpolated directly into the output filename. If session_id can contain path separators (e.g., user-supplied IDs), this can lead to path traversal and writing outside output_dir. Consider sanitizing session_id to a safe filename (e.g., allowlist [A-Za-z0-9._-] and replace others) or hashing it for the persisted artifact name.

Copilot uses AI. Check for mistakes.
"reflection_output": reflection_output,
"lesson_output": lesson_output,
}
out_path.write_text(json.dumps(bundle, ensure_ascii=False, indent=2), encoding="utf-8")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

json.dumps(bundle, ...) can raise TypeError if payload, reflection_output, or lesson_output contain non-JSON-serializable values (e.g., message objects). Since this runs in a background task, it will cause lesson persistence to fail entirely. Consider using a safe JSON dump (e.g., default=str or a pre-normalization step) to guarantee persistence.

Suggested change
out_path.write_text(json.dumps(bundle, ensure_ascii=False, indent=2), encoding="utf-8")
out_path.write_text(
json.dumps(bundle, ensure_ascii=False, indent=2, default=str),
encoding="utf-8",
)

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +12
reflection_agent_spec_path: str = "examples/configs/specs/learning/reflection_agent.yaml"
lesson_agent_spec_path: str = "examples/configs/specs/learning/lesson_agent.yaml"
output_dir: str = "artifacts/learning_lessons"
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The default spec paths point into examples/…, which may not exist in packaged/distributed installs or when the working directory differs. Consider making these required (no defaults), resolving them relative to the installed package via resources, or documenting that callers must override them when enabling learning.

Suggested change
reflection_agent_spec_path: str = "examples/configs/specs/learning/reflection_agent.yaml"
lesson_agent_spec_path: str = "examples/configs/specs/learning/lesson_agent.yaml"
output_dir: str = "artifacts/learning_lessons"
reflection_agent_spec_path: str | None = None
lesson_agent_spec_path: str | None = None
output_dir: str | None = None

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants