deploy
This commit is contained in:
4745
.a2a/agent.dsl.json
Normal file
4745
.a2a/agent.dsl.json
Normal file
File diff suppressed because it is too large
Load Diff
4
a2a.yaml
4
a2a.yaml
@@ -1,7 +1,9 @@
|
|||||||
name: openpannel
|
name: openpannel
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
entrypoint: agent:Openpannel
|
entrypoint: agent:Openpannel
|
||||||
description: Composite OpenAPI agent generated from 2 OpenAPI specs.
|
description: OpenPanel analytics agent for the A2A Cloud OpenPanel deployment. Includes
|
||||||
|
manage/admin operations and project analytics operations from the OpenPanel OpenAPI
|
||||||
|
specs.
|
||||||
runtime:
|
runtime:
|
||||||
resources:
|
resources:
|
||||||
cpu: 200m
|
cpu: 200m
|
||||||
|
|||||||
130
agent.py
130
agent.py
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import builtins
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -1625,6 +1626,135 @@ class Openpannel(A2AAgent):
|
|||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@skill(
|
||||||
|
name="get_project_geo_from_events",
|
||||||
|
description=(
|
||||||
|
"Summarize OpenPanel visitor geography from raw exported events. "
|
||||||
|
"Use this instead of the stale /insights/{projectId}/traffic/geo route."
|
||||||
|
),
|
||||||
|
tags=("Insights", "Geo"),
|
||||||
|
timeout_seconds=120,
|
||||||
|
)
|
||||||
|
async def get_project_geo_from_events(
|
||||||
|
self,
|
||||||
|
ctx: RunContext,
|
||||||
|
projectId: str,
|
||||||
|
start: str | None = None,
|
||||||
|
end: str | None = None,
|
||||||
|
range: str = "today",
|
||||||
|
limit: int = 250,
|
||||||
|
max_pages: int = 4,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
per_page = max(1, min(int(limit), 1000))
|
||||||
|
pages = max(1, min(int(max_pages), 10))
|
||||||
|
aggregate: dict[str, dict[str, Any]] = {}
|
||||||
|
samples: list[dict[str, Any]] = []
|
||||||
|
total_events = 0
|
||||||
|
|
||||||
|
for page in builtins.range(1, pages + 1):
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
"projectId": projectId,
|
||||||
|
"page": page,
|
||||||
|
"limit": per_page,
|
||||||
|
}
|
||||||
|
if start or end:
|
||||||
|
if start:
|
||||||
|
params["start"] = start
|
||||||
|
if end:
|
||||||
|
params["end"] = end
|
||||||
|
else:
|
||||||
|
params["range"] = range
|
||||||
|
|
||||||
|
response = await self._request(
|
||||||
|
ctx,
|
||||||
|
"get_export_events",
|
||||||
|
parameters=params,
|
||||||
|
body=None,
|
||||||
|
)
|
||||||
|
if not response.get("ok"):
|
||||||
|
return response
|
||||||
|
|
||||||
|
result = response.get("result") if isinstance(response, dict) else None
|
||||||
|
data = result.get("data", []) if isinstance(result, dict) else []
|
||||||
|
meta = result.get("meta", {}) if isinstance(result, dict) else {}
|
||||||
|
if not isinstance(data, list) or not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
total_events += len(data)
|
||||||
|
for event in data:
|
||||||
|
if not isinstance(event, dict):
|
||||||
|
continue
|
||||||
|
country = self._clean_geo_value(event.get("country"))
|
||||||
|
region = self._clean_geo_value(event.get("region"))
|
||||||
|
city = self._clean_geo_value(event.get("city"))
|
||||||
|
key = "|".join([country, region, city])
|
||||||
|
bucket = aggregate.setdefault(
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
"country": country or None,
|
||||||
|
"region": region or None,
|
||||||
|
"city": city or None,
|
||||||
|
"events": 0,
|
||||||
|
"sessions": set(),
|
||||||
|
"latest": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bucket["events"] += 1
|
||||||
|
session_id = event.get("sessionId") or event.get("session_id")
|
||||||
|
if session_id:
|
||||||
|
bucket["sessions"].add(str(session_id))
|
||||||
|
created_at = event.get("createdAt") or event.get("created_at")
|
||||||
|
if created_at and (bucket["latest"] is None or str(created_at) > str(bucket["latest"])):
|
||||||
|
bucket["latest"] = created_at
|
||||||
|
if len(samples) < 10:
|
||||||
|
samples.append(
|
||||||
|
{
|
||||||
|
"createdAt": created_at,
|
||||||
|
"name": event.get("name"),
|
||||||
|
"path": event.get("path"),
|
||||||
|
"country": country or None,
|
||||||
|
"region": region or None,
|
||||||
|
"city": city or None,
|
||||||
|
"browser": event.get("browser"),
|
||||||
|
"os": event.get("os"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(meta, dict):
|
||||||
|
current = int(meta.get("current") or page)
|
||||||
|
page_count = int(meta.get("pages") or page)
|
||||||
|
if current >= page_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for bucket in aggregate.values():
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"country": bucket["country"],
|
||||||
|
"region": bucket["region"],
|
||||||
|
"city": bucket["city"],
|
||||||
|
"events": bucket["events"],
|
||||||
|
"sessions": len(bucket["sessions"]),
|
||||||
|
"latest": bucket["latest"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rows.sort(key=lambda row: (row["latest"] or "", row["events"]), reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"operation_id": "get_project_geo_from_events",
|
||||||
|
"projectId": projectId,
|
||||||
|
"range": {"start": start, "end": end, "range": range},
|
||||||
|
"events_scanned": total_events,
|
||||||
|
"geo": rows,
|
||||||
|
"sample_events": samples,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _clean_geo_value(value: Any) -> str:
|
||||||
|
text = str(value or "").replace("\x00", "").strip()
|
||||||
|
return text
|
||||||
|
|
||||||
def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]:
|
def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]:
|
||||||
tools: list[StructuredTool] = []
|
tools: list[StructuredTool] = []
|
||||||
for operation_id, operation in OPERATIONS.items():
|
for operation_id, operation in OPERATIONS.items():
|
||||||
|
|||||||
Reference in New Issue
Block a user