This commit is contained in:
a2a-platform
2026-06-29 01:34:41 +00:00
parent a519e7b558
commit dfa7bb3bc4
6 changed files with 90 additions and 13 deletions

View File

@@ -5,6 +5,7 @@ import json
import re
from pathlib import Path
from typing import Any
from urllib.parse import quote
import httpx
from a2a_pack.deepagents import create_a2a_deep_agent
@@ -53,8 +54,11 @@ LEARNHOUSE_CONTEXT = (
"create at least one activity in each chapter using that chapter's specific "
"chapter_id. Do not attach all activities to the first chapter. For visible "
"lesson content, create activities with activity_type TYPE_DYNAMIC, "
"activity_sub_type SUBTYPE_DYNAMIC_MARKDOWN, markdown content/details, "
"lock_type public, and published=true unless the user requests drafts. "
"activity_sub_type SUBTYPE_DYNAMIC_MARKDOWN, content.markdown_url pointing "
"to fetchable markdown, lock_type public, and published=true unless the "
"user requests drafts. The renderer ignores inline content.body and "
"content.markdown unless content.markdown_url is also configured; when no "
"external markdown URL is available, use a data:text/markdown URL. "
"After writing course content, verify every chapter has at least one "
"activity through course metadata or the chapter activity endpoint. When "
"updating activities, do not send published_version; this LearnHouse "
@@ -77,7 +81,7 @@ class OperationInput(BaseModel):
class LearnhouseAgent(A2AAgent):
name = "learnhouse-agent"
description = "LearnHouse is an open-source platform tailored for learning experiences."
version = "1.2.12"
version = "1.2.13"
consumer_setup = ConsumerSetup.from_fields(
ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://learnhouse.a2acloud.io).", required=False, input_type="url"),
ConsumerSetupField.secret(
@@ -309,8 +313,10 @@ class LearnhouseAgent(A2AAgent):
parts.append(
"For LearnHouse activity creation, send JSON fields name, "
"chapter_id, activity_type, activity_sub_type, content, "
"details, lock_type, and published. Use the target "
"chapter's own chapter_id; do not reuse the first chapter ID."
"details, lock_type, and published. Markdown activities "
"must include content.markdown_url. If only inline markdown "
"is available, this agent converts it to a data:text/markdown URL. "
"Use the target chapter's own chapter_id; do not reuse the first chapter ID."
)
if _is_learnhouse_update_activity(operation):
parts.append(
@@ -348,6 +354,7 @@ class LearnhouseAgent(A2AAgent):
body = json.loads(stripped)
except ValueError:
pass
body = _normalize_learnhouse_activity_body(operation, body)
if _operation_uses_multipart(operation):
request_kwargs["data"] = body
else:
@@ -540,6 +547,70 @@ def _is_learnhouse_update_activity(operation: dict[str, Any]) -> bool:
)
def _normalize_learnhouse_activity_body(operation: dict[str, Any], body: Any) -> Any:
if not (
_is_learnhouse_create_activity(operation)
or _is_learnhouse_update_activity(operation)
):
return body
if not isinstance(body, dict):
return body
normalized = dict(body)
if _is_learnhouse_update_activity(operation):
normalized.pop("published_version", None)
content = normalized.get("content")
if not isinstance(content, dict):
content = {}
else:
content = dict(content)
details = normalized.get("details")
details_dict = details if isinstance(details, dict) else {}
metadata = normalized.get("extra_metadata")
metadata_dict = metadata if isinstance(metadata, dict) else {}
sub_type = str(normalized.get("activity_sub_type") or content.get("activity_sub_type") or "")
has_markdown_hint = (
sub_type == "SUBTYPE_DYNAMIC_MARKDOWN"
or any(key in content for key in ("markdown_url", "markdown", "body", "text"))
or any(key in details_dict for key in ("markdown", "body", "text"))
or any(key in metadata_dict for key in ("markdown", "body", "text"))
)
if not has_markdown_hint:
return normalized
normalized.setdefault("activity_type", "TYPE_DYNAMIC")
normalized["activity_sub_type"] = "SUBTYPE_DYNAMIC_MARKDOWN"
markdown_url = str(content.get("markdown_url") or "").strip()
if not markdown_url:
markdown_text = _extract_markdown_text(content, details_dict, metadata_dict, normalized)
if markdown_text:
content["markdown_url"] = _markdown_data_url(markdown_text)
normalized["content"] = content
if isinstance(details, dict):
normalized["details"] = dict(details, content_type=details.get("content_type") or "markdown")
elif details is None:
normalized["details"] = {"content_type": "markdown"}
return normalized
def _extract_markdown_text(*sources: dict[str, Any]) -> str:
for source in sources:
for key in ("markdown", "body", "text", "content"):
value = source.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
title = str(sources[-1].get("name") or "Lesson").strip()
return f"# {title}\n"
def _markdown_data_url(markdown_text: str) -> str:
return "data:text/markdown;charset=utf-8," + quote(markdown_text, safe="")
def _apply_learnhouse_defaults(
operation: dict[str, Any],
parameters: dict[str, Any],