Skip to content

ChangelogUtils

ChangelogUtils provides helper methods for working with CXAS app changelog data. It builds on top of the Changelogs class to offer higher-level operations: formatting changelog entries for display, filtering by resource type or time range, and converting the raw changelog stream into a structured pandas DataFrame.

Use this class when you want to generate a human-readable release summary, audit changes between two deployments, or feed changelog data into a reporting dashboard.

Quick Example

from cxas_scrapi import ChangelogUtils

app_name = "projects/my-project/locations/us/apps/my-app-id"
cu = ChangelogUtils(app_name=app_name)

# Get a formatted summary of recent changes
summary = cu.get_changelog_summary(limit=20)
print(summary)

# Convert to a DataFrame for analysis or export
df = cu.changelogs_to_dataframe()
print(df[["create_time", "action", "resource_display_name", "user_email"]].head(10))

# Filter to only instruction changes
instruction_changes = df[df["resource_type"] == "Agent"]
print(instruction_changes)

Reference

ChangelogUtils

get_changelogs_since_last_version staticmethod

get_changelogs_since_last_version(all_changelogs, versions)

Retrieves changelogs created after the most recent app version.

Source code in src/cxas_scrapi/utils/changelog_utils.py
@staticmethod
def get_changelogs_since_last_version(
    all_changelogs: List[Dict[str, Any]], versions: List[Any]
) -> List[Dict[str, Any]]:
    """Retrieves changelogs created after the most recent app version."""
    if not versions:
        print("No versions found. Returning all changelogs.")
        return all_changelogs
    try:
        valid_versions = [
            v for v in versions if isinstance(v, dict) and "createTime" in v
        ]
        if not valid_versions:
            print(
                "No valid versions with createTime found. Returning all "
                "changelogs."
            )
            return all_changelogs

        latest_version = max(
            valid_versions, key=lambda version: version["createTime"]
        )
        latest_version_timestamp_str = latest_version["createTime"]
        print(
            f"Latest version was created at: {latest_version_timestamp_str}"
        )
        latest_version_dt = datetime.datetime.fromisoformat(
            latest_version_timestamp_str.replace("Z", "+00:00")
        )
        recent_changelogs = [
            cl
            for cl in all_changelogs
            if isinstance(cl, dict)
            and "createTime" in cl
            and datetime.datetime.fromisoformat(
                cl["createTime"].replace("Z", "+00:00")
            )
            > latest_version_dt
        ]
        return recent_changelogs
    except (ValueError, KeyError, TypeError) as e:
        print(
            f"Error processing version or changelog timestamps: {e}. "
            f"Returning all changelogs."
        )
        return all_changelogs

summarize_changelogs staticmethod

summarize_changelogs(vertex_client_or_project, changelogs, project_id=None)

Summarizes each non-evaluation changelog into a simple, specific one-liner.

Source code in src/cxas_scrapi/utils/changelog_utils.py
@staticmethod
def summarize_changelogs(
    vertex_client_or_project: Any,
    changelogs: List[Dict[str, Any]],
    project_id: str = None,
) -> str:
    """Summarizes each non-evaluation changelog into a simple, specific
    one-liner."""
    resource_types_to_exclude = [
        "Version",
        "AppVersion",
        "Evaluation",
        "EvaluationRun",
    ]
    filtered_changelogs = [
        cl
        for cl in changelogs
        if cl.get("resourceType") not in resource_types_to_exclude
    ]

    if not filtered_changelogs:
        return "No user-facing changes to summarize."

    # Format each changelog entry, potentially producing multi-line
    # strings for updates
    formatted_log_entries = [
        ChangelogUtils._format_changelog_for_prompt(cl)
        for cl in filtered_changelogs
    ]

    # Combine and number the entries for the prompt
    changelog_context = ""
    entry_number = 1
    for entry in formatted_log_entries:
        # Add numbering only to the start of each logical entry
        lines = entry.strip().split("\n")
        changelog_context += f"{entry_number}. {lines[0]}\n"
        if len(lines) > 1:
            changelog_context += (
                "\n".join([f"   {line}" for line in lines[1:]]) + "\n"
            )  # Indent Original/New
        entry_number += 1

    if (
        not changelog_context.strip()
    ):  # Check if context became empty after formatting
        return "No user-facing changes to summarize."

    prompt = f"""
    You are an AI assistant that analyzes technical log entries,
    specifically focusing on 'Update' actions by comparing 'Original' and
    'New' configuration snippets. Your goal is to generate a concise,
    single-line summary describing the *exact* change that occurred for
    each entry.

    Rules:
    - **CRITICAL**: Provide one summary line for each numbered entry in
      the input. Maintain a 1-to-1 correspondence.
    - For 'Update' entries with 'Original' and 'New' snippets: Compare
      them carefully to identify the precise difference. Describe only
      that specific change (e.g., "Disabled barge-in", "Added variable
      'X'", "Updated agent instructions", "Added tool 'Y'").
    - For 'Create' or 'Delete' entries (or 'Update' entries without
      Original/New comparison data): Generate a summary based on the
      Action, ResourceType, Name, and Description provided (e.g.,
      "Created tool 'get_weather'", "Deleted agent 'old_agent'").
    - Be specific. Avoid generic phrases like "Updated settings" or
      "Changed configuration". State *what* was updated.
    - The final output must be a bulleted list, starting each line with
      '-'.

    Here is the raw changelog data to summarize:
    ---
    {changelog_context}
    ---

    Provide the specific one-line summary for each numbered entry:
    """

    try:
        # Handle if the user passes the vertex framework client or strings

        if isinstance(vertex_client_or_project, GeminiGenerate):
            response_text = vertex_client_or_project.generate(
                prompt=prompt, model_name="gemini-2.5-flash"
            )
        elif hasattr(vertex_client_or_project, "models"):
            response = vertex_client_or_project.models.generate_content(
                model="gemini-2.5-flash", contents=prompt
            )
            response_text = response.text
        else:
            cl = GeminiGenerate(
                project_id=project_id,
                location="us-central1",
                model_name="gemini-2.5-flash",
            )
            response_text = cl.generate(prompt=prompt)

        # Basic post-processing to clean up potential numbering/extra
        # whitespace
        lines = response_text.strip().split("\n") if response_text else []
        cleaned_lines = [
            line.strip() for line in lines if line.strip().startswith("-")
        ]
        return "\n".join(cleaned_lines)
    except Exception as e:
        return f"An error occurred while generating the summary: {e}"