agentberlin 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ """Agent Berlin Python SDK.
2
+
3
+ A Python SDK for Agent Berlin - AI-powered SEO and AEO automation.
4
+
5
+ Example:
6
+ from agentberlin import AgentBerlin
7
+
8
+ # Set AGENTBERLIN_TOKEN environment variable or pass token directly
9
+ client = AgentBerlin()
10
+
11
+ # Get analytics
12
+ analytics = client.analytics.get(project_domain="example.com")
13
+
14
+ # Search pages
15
+ pages = client.pages.search(project_domain="example.com", query="SEO tips")
16
+
17
+ # Search keywords
18
+ keywords = client.keywords.search(project_domain="example.com", query="marketing")
19
+
20
+ # Get brand profile
21
+ profile = client.brand.get_profile(project_domain="example.com")
22
+
23
+ # Fetch SERP results
24
+ serp = client.serp.fetch(query="best seo tools")
25
+ """
26
+
27
+ from .client import AgentBerlin
28
+ from .exceptions import (
29
+ AgentBerlinAPIError,
30
+ AgentBerlinAuthenticationError,
31
+ AgentBerlinConnectionError,
32
+ AgentBerlinError,
33
+ AgentBerlinNotFoundError,
34
+ AgentBerlinRateLimitError,
35
+ AgentBerlinServerError,
36
+ AgentBerlinValidationError,
37
+ )
38
+
39
+ __version__ = "0.9.0"
40
+
41
+ __all__ = [
42
+ "AgentBerlin",
43
+ "AgentBerlinError",
44
+ "AgentBerlinAuthenticationError",
45
+ "AgentBerlinAPIError",
46
+ "AgentBerlinNotFoundError",
47
+ "AgentBerlinRateLimitError",
48
+ "AgentBerlinServerError",
49
+ "AgentBerlinValidationError",
50
+ "AgentBerlinConnectionError",
51
+ ]
agentberlin/_http.py ADDED
@@ -0,0 +1,193 @@
1
+ """Internal HTTP client for Agent Berlin SDK."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import requests
6
+
7
+ from .exceptions import (
8
+ AgentBerlinAPIError,
9
+ AgentBerlinAuthenticationError,
10
+ AgentBerlinConnectionError,
11
+ AgentBerlinNotFoundError,
12
+ AgentBerlinRateLimitError,
13
+ AgentBerlinServerError,
14
+ )
15
+
16
+ __version__ = "0.1.0"
17
+
18
+
19
+ class HTTPClient:
20
+ """Internal HTTP client with authentication and error handling."""
21
+
22
+ def __init__(
23
+ self,
24
+ token: str,
25
+ base_url: str,
26
+ timeout: int = 30,
27
+ ) -> None:
28
+ self._token = token
29
+ self._base_url = base_url.rstrip("/")
30
+ self._timeout = timeout
31
+ self._session = requests.Session()
32
+ self._session.headers.update(
33
+ {
34
+ "Authorization": f"Bearer {token}",
35
+ "Content-Type": "application/json",
36
+ "User-Agent": f"agentberlin-python/{__version__}",
37
+ }
38
+ )
39
+
40
+ def post(
41
+ self,
42
+ path: str,
43
+ json: Optional[Dict[str, Any]] = None,
44
+ ) -> Dict[str, Any]:
45
+ """Make a POST request.
46
+
47
+ Args:
48
+ path: API endpoint path.
49
+ json: Request body as dict.
50
+
51
+ Returns:
52
+ Response data as dict.
53
+
54
+ Raises:
55
+ AgentBerlinAPIError: On API errors.
56
+ AgentBerlinConnectionError: On network errors.
57
+ """
58
+ url = f"{self._base_url}{path}"
59
+
60
+ try:
61
+ response = self._session.post(
62
+ url,
63
+ json=json,
64
+ timeout=self._timeout,
65
+ )
66
+ except requests.exceptions.Timeout:
67
+ raise AgentBerlinConnectionError(f"Request timed out after {self._timeout}s")
68
+ except requests.exceptions.ConnectionError as e:
69
+ raise AgentBerlinConnectionError(f"Connection error: {e}")
70
+ except requests.exceptions.RequestException as e:
71
+ raise AgentBerlinConnectionError(f"Request failed: {e}")
72
+
73
+ return self._handle_response(response)
74
+
75
+ def get(
76
+ self,
77
+ path: str,
78
+ params: Optional[Dict[str, Any]] = None,
79
+ ) -> Dict[str, Any]:
80
+ """Make a GET request.
81
+
82
+ Args:
83
+ path: API endpoint path.
84
+ params: Query parameters.
85
+
86
+ Returns:
87
+ Response data as dict.
88
+
89
+ Raises:
90
+ AgentBerlinAPIError: On API errors.
91
+ AgentBerlinConnectionError: On network errors.
92
+ """
93
+ url = f"{self._base_url}{path}"
94
+
95
+ try:
96
+ response = self._session.get(
97
+ url,
98
+ params=params,
99
+ timeout=self._timeout,
100
+ )
101
+ except requests.exceptions.Timeout:
102
+ raise AgentBerlinConnectionError(f"Request timed out after {self._timeout}s")
103
+ except requests.exceptions.ConnectionError as e:
104
+ raise AgentBerlinConnectionError(f"Connection error: {e}")
105
+ except requests.exceptions.RequestException as e:
106
+ raise AgentBerlinConnectionError(f"Request failed: {e}")
107
+
108
+ return self._handle_response(response)
109
+
110
+ def patch(
111
+ self,
112
+ path: str,
113
+ json: Optional[Dict[str, Any]] = None,
114
+ ) -> Dict[str, Any]:
115
+ """Make a PATCH request.
116
+
117
+ Args:
118
+ path: API endpoint path.
119
+ json: Request body as dict.
120
+
121
+ Returns:
122
+ Response data as dict.
123
+
124
+ Raises:
125
+ AgentBerlinAPIError: On API errors.
126
+ AgentBerlinConnectionError: On network errors.
127
+ """
128
+ url = f"{self._base_url}{path}"
129
+
130
+ try:
131
+ response = self._session.patch(
132
+ url,
133
+ json=json,
134
+ timeout=self._timeout,
135
+ )
136
+ except requests.exceptions.Timeout:
137
+ raise AgentBerlinConnectionError(f"Request timed out after {self._timeout}s")
138
+ except requests.exceptions.ConnectionError as e:
139
+ raise AgentBerlinConnectionError(f"Connection error: {e}")
140
+ except requests.exceptions.RequestException as e:
141
+ raise AgentBerlinConnectionError(f"Request failed: {e}")
142
+
143
+ return self._handle_response(response)
144
+
145
+ def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
146
+ """Handle response and raise appropriate exceptions."""
147
+ # Success
148
+ if response.ok:
149
+ if response.content:
150
+ return response.json() # type: ignore[no-any-return]
151
+ return {}
152
+
153
+ # Parse error response
154
+ try:
155
+ error_data = response.json()
156
+ message = error_data.get("message", error_data.get("error", "Unknown error"))
157
+ error_code = error_data.get("code")
158
+ except Exception:
159
+ message = response.text or f"HTTP {response.status_code}"
160
+ error_code = None
161
+ error_data = {}
162
+
163
+ # Map status codes to exceptions
164
+ if response.status_code == 401:
165
+ raise AgentBerlinAuthenticationError(message, details=error_data)
166
+ elif response.status_code == 404:
167
+ raise AgentBerlinNotFoundError(
168
+ message,
169
+ status_code=404,
170
+ error_code=error_code,
171
+ details=error_data,
172
+ )
173
+ elif response.status_code == 429:
174
+ retry_after = response.headers.get("Retry-After")
175
+ raise AgentBerlinRateLimitError(
176
+ message,
177
+ retry_after=int(retry_after) if retry_after else None,
178
+ details=error_data,
179
+ )
180
+ elif response.status_code >= 500:
181
+ raise AgentBerlinServerError(
182
+ message,
183
+ status_code=response.status_code,
184
+ error_code=error_code,
185
+ details=error_data,
186
+ )
187
+ else:
188
+ raise AgentBerlinAPIError(
189
+ message,
190
+ status_code=response.status_code,
191
+ error_code=error_code,
192
+ details=error_data,
193
+ )
agentberlin/client.py ADDED
@@ -0,0 +1,85 @@
1
+ """Main client class for Agent Berlin SDK.
2
+
3
+ Example:
4
+ from agentberlin import AgentBerlin
5
+
6
+ client = AgentBerlin() # Uses AGENTBERLIN_TOKEN env var
7
+
8
+ # Get analytics
9
+ analytics = client.analytics.get(project_domain="example.com")
10
+ print(f"Traffic: {analytics.traffic.total_sessions}")
11
+
12
+ # Search pages
13
+ pages = client.pages.search(project_domain="example.com", query="SEO tips")
14
+ for page in pages.pages:
15
+ print(f" - {page.title}: {page.url}")
16
+ """
17
+
18
+ import os
19
+ from typing import Optional
20
+
21
+ from ._http import HTTPClient
22
+ from .config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, Config
23
+ from .exceptions import AgentBerlinAuthenticationError
24
+ from .resources.analytics import AnalyticsResource
25
+ from .resources.brand import BrandResource
26
+ from .resources.keywords import KeywordsResource
27
+ from .resources.pages import PagesResource
28
+ from .resources.serp import SERPResource
29
+
30
+
31
+ class AgentBerlin:
32
+ """Client for the Agent Berlin API.
33
+
34
+ Args:
35
+ token: API token. If not provided, reads from AGENTBERLIN_TOKEN env var.
36
+ base_url: Base URL for the API. Defaults to https://backend.agentberlin.ai/sdk
37
+ timeout: Request timeout in seconds. Defaults to 30.
38
+
39
+ Raises:
40
+ AgentBerlinAuthenticationError: If no token is provided or found in env.
41
+
42
+ Attributes:
43
+ analytics: Analytics resource for fetching analytics data.
44
+ pages: Pages resource for searching and getting page details.
45
+ keywords: Keywords resource for semantic keyword search.
46
+ brand: Brand resource for managing brand profiles.
47
+ serp: SERP resource for fetching Google search results.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ token: Optional[str] = None,
53
+ base_url: Optional[str] = None,
54
+ timeout: int = DEFAULT_TIMEOUT,
55
+ ) -> None:
56
+ # Get token from parameter or environment
57
+ self._token = token or os.environ.get("AGENTBERLIN_TOKEN")
58
+ if not self._token:
59
+ raise AgentBerlinAuthenticationError(
60
+ "No API token provided. Set AGENTBERLIN_TOKEN environment variable "
61
+ "or pass token parameter to AgentBerlin()."
62
+ )
63
+
64
+ # Initialize config
65
+ self._config = Config(
66
+ base_url=base_url or DEFAULT_BASE_URL,
67
+ timeout=timeout,
68
+ )
69
+
70
+ # Initialize HTTP client
71
+ self._http = HTTPClient(
72
+ token=self._token,
73
+ base_url=self._config.base_url,
74
+ timeout=self._config.timeout,
75
+ )
76
+
77
+ # Initialize resources
78
+ self.analytics = AnalyticsResource(self._http)
79
+ self.pages = PagesResource(self._http)
80
+ self.keywords = KeywordsResource(self._http)
81
+ self.brand = BrandResource(self._http)
82
+ self.serp = SERPResource(self._http)
83
+
84
+ def __repr__(self) -> str:
85
+ return f"AgentBerlin(base_url='{self._config.base_url}')"
agentberlin/config.py ADDED
@@ -0,0 +1,19 @@
1
+ """Configuration for the Agent Berlin SDK."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ DEFAULT_BASE_URL = "https://backend.agentberlin.ai/sdk"
6
+ DEFAULT_TIMEOUT = 30
7
+
8
+
9
+ @dataclass
10
+ class Config:
11
+ """SDK configuration.
12
+
13
+ Attributes:
14
+ base_url: Base URL for the Agent Berlin API.
15
+ timeout: Request timeout in seconds.
16
+ """
17
+
18
+ base_url: str = DEFAULT_BASE_URL
19
+ timeout: int = DEFAULT_TIMEOUT
@@ -0,0 +1,88 @@
1
+ """Custom exceptions for the Agent Berlin SDK.
2
+
3
+ Exception hierarchy:
4
+ AgentBerlinError (base)
5
+ ├── AgentBerlinAuthenticationError - Token missing or invalid
6
+ ├── AgentBerlinAPIError - API returned an error
7
+ │ ├── AgentBerlinNotFoundError - Resource not found (404)
8
+ │ ├── AgentBerlinRateLimitError - Rate limit exceeded (429)
9
+ │ └── AgentBerlinServerError - Server error (5xx)
10
+ ├── AgentBerlinValidationError - Invalid parameters
11
+ └── AgentBerlinConnectionError - Network/connection issues
12
+ """
13
+
14
+ from typing import Any, Dict, Optional
15
+
16
+
17
+ class AgentBerlinError(Exception):
18
+ """Base exception for all Agent Berlin SDK errors."""
19
+
20
+ def __init__(self, message: str, details: Optional[Dict[str, Any]] = None) -> None:
21
+ super().__init__(message)
22
+ self.message = message
23
+ self.details = details or {}
24
+
25
+
26
+ class AgentBerlinAuthenticationError(AgentBerlinError):
27
+ """Raised when authentication fails or token is missing."""
28
+
29
+ pass
30
+
31
+
32
+ class AgentBerlinAPIError(AgentBerlinError):
33
+ """Raised when the API returns an error response."""
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ status_code: int,
39
+ error_code: Optional[str] = None,
40
+ details: Optional[Dict[str, Any]] = None,
41
+ ) -> None:
42
+ super().__init__(message, details)
43
+ self.status_code = status_code
44
+ self.error_code = error_code
45
+
46
+
47
+ class AgentBerlinNotFoundError(AgentBerlinAPIError):
48
+ """Raised when a requested resource is not found (404)."""
49
+
50
+ pass
51
+
52
+
53
+ class AgentBerlinRateLimitError(AgentBerlinAPIError):
54
+ """Raised when rate limit is exceeded (429)."""
55
+
56
+ def __init__(
57
+ self,
58
+ message: str,
59
+ retry_after: Optional[int] = None,
60
+ details: Optional[Dict[str, Any]] = None,
61
+ ) -> None:
62
+ super().__init__(message, status_code=429, details=details)
63
+ self.retry_after = retry_after
64
+
65
+
66
+ class AgentBerlinServerError(AgentBerlinAPIError):
67
+ """Raised when the server returns a 5xx error."""
68
+
69
+ pass
70
+
71
+
72
+ class AgentBerlinValidationError(AgentBerlinError):
73
+ """Raised when request parameters fail validation."""
74
+
75
+ def __init__(
76
+ self,
77
+ message: str,
78
+ field: Optional[str] = None,
79
+ details: Optional[Dict[str, Any]] = None,
80
+ ) -> None:
81
+ super().__init__(message, details)
82
+ self.field = field
83
+
84
+
85
+ class AgentBerlinConnectionError(AgentBerlinError):
86
+ """Raised when there's a network or connection error."""
87
+
88
+ pass
@@ -0,0 +1,53 @@
1
+ """Pydantic models for Agent Berlin API responses."""
2
+
3
+ from .analytics import (
4
+ AnalyticsResponse,
5
+ ChannelBreakdown,
6
+ CompetitorSummary,
7
+ DailyTraffic,
8
+ DataRange,
9
+ TopicSummary,
10
+ TrafficData,
11
+ VisibilityData,
12
+ VisibilityPoint,
13
+ )
14
+ from .brand import BrandProfileResponse, BrandProfileUpdateResponse
15
+ from .search import (
16
+ KeywordResult,
17
+ KeywordSearchResponse,
18
+ PageDetailResponse,
19
+ PageLink,
20
+ PageLinksDetail,
21
+ PageResult,
22
+ PageSearchResponse,
23
+ PageTopicInfo,
24
+ )
25
+ from .serp import SERPResponse, SERPResult
26
+
27
+ __all__ = [
28
+ # Analytics
29
+ "AnalyticsResponse",
30
+ "VisibilityData",
31
+ "VisibilityPoint",
32
+ "TrafficData",
33
+ "ChannelBreakdown",
34
+ "DailyTraffic",
35
+ "TopicSummary",
36
+ "CompetitorSummary",
37
+ "DataRange",
38
+ # Search
39
+ "PageSearchResponse",
40
+ "PageResult",
41
+ "KeywordSearchResponse",
42
+ "KeywordResult",
43
+ "PageDetailResponse",
44
+ "PageLinksDetail",
45
+ "PageLink",
46
+ "PageTopicInfo",
47
+ # Brand
48
+ "BrandProfileResponse",
49
+ "BrandProfileUpdateResponse",
50
+ # SERP
51
+ "SERPResponse",
52
+ "SERPResult",
53
+ ]
@@ -0,0 +1,87 @@
1
+ """Pydantic models for analytics API responses."""
2
+
3
+ from typing import List
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class VisibilityPoint(BaseModel):
9
+ """Single point in visibility history."""
10
+
11
+ date: str
12
+ percentage: float
13
+
14
+
15
+ class VisibilityData(BaseModel):
16
+ """Visibility metrics for a domain."""
17
+
18
+ current_percentage: float
19
+ ranking_stability: float
20
+ share_of_voice: float
21
+ history: List[VisibilityPoint] = Field(default_factory=list)
22
+
23
+
24
+ class ChannelBreakdown(BaseModel):
25
+ """Traffic breakdown by channel."""
26
+
27
+ direct: int = 0
28
+ organic_search: int = 0
29
+ referral: int = 0
30
+ organic_social: int = 0
31
+ llm: int = 0
32
+ other: int = 0
33
+
34
+
35
+ class DailyTraffic(BaseModel):
36
+ """Daily traffic data point."""
37
+
38
+ date: str
39
+ sessions: int
40
+ llm_sessions: int
41
+
42
+
43
+ class TrafficData(BaseModel):
44
+ """Traffic metrics for a domain."""
45
+
46
+ total_sessions: int
47
+ llm_sessions: int
48
+ channel_breakdown: ChannelBreakdown
49
+ daily_trend: List[DailyTraffic] = Field(default_factory=list)
50
+
51
+
52
+ class TopicSummary(BaseModel):
53
+ """Summary of a topic's performance."""
54
+
55
+ name: str
56
+ appearances: int
57
+ avg_position: float
58
+ topical_authority: float
59
+ trend: str # "up", "down", "stable"
60
+
61
+
62
+ class CompetitorSummary(BaseModel):
63
+ """Summary of competitor visibility."""
64
+
65
+ name: str
66
+ visibility: float
67
+ share_of_voice: float
68
+
69
+
70
+ class DataRange(BaseModel):
71
+ """Date range for the analytics data."""
72
+
73
+ start: str
74
+ end: str
75
+
76
+
77
+ class AnalyticsResponse(BaseModel):
78
+ """Complete analytics response for a domain."""
79
+
80
+ domain: str
81
+ domain_authority: int
82
+ visibility: VisibilityData
83
+ traffic: TrafficData
84
+ topics: List[TopicSummary] = Field(default_factory=list)
85
+ competitors: List[CompetitorSummary] = Field(default_factory=list)
86
+ data_range: DataRange
87
+ last_updated: str
@@ -0,0 +1,40 @@
1
+ """Pydantic models for brand profile API responses."""
2
+
3
+ from typing import Any, List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class BrandProfileResponse(BaseModel):
9
+ """Brand profile configuration for a project."""
10
+
11
+ domain: str
12
+ name: str
13
+ context: str
14
+ search_analysis_context: Optional[str] = None
15
+ domain_authority: int
16
+ competitors: List[str] = Field(default_factory=list)
17
+ industries: List[str] = Field(default_factory=list)
18
+ business_models: List[str] = Field(default_factory=list)
19
+ company_size: str
20
+ target_customer_segments: List[str] = Field(default_factory=list)
21
+ geographies: List[str] = Field(default_factory=list)
22
+ personas: List[str] = Field(default_factory=list)
23
+ sitemaps: List[str] = Field(default_factory=list)
24
+ profile_urls: List[str] = Field(default_factory=list)
25
+
26
+
27
+ class BrandProfileUpdateField(BaseModel):
28
+ """Updated field information."""
29
+
30
+ field: str
31
+ value: Any
32
+
33
+
34
+ class BrandProfileUpdateResponse(BaseModel):
35
+ """Response for brand profile update."""
36
+
37
+ success: bool
38
+ domain: str
39
+ mode: str
40
+ updated: BrandProfileUpdateField
@@ -0,0 +1,85 @@
1
+ """Pydantic models for search API responses (pages, keywords)."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ # Page search models
8
+
9
+
10
+ class PageResult(BaseModel):
11
+ """Single page in search results."""
12
+
13
+ url: str
14
+ title: str
15
+
16
+
17
+ class PageSearchResponse(BaseModel):
18
+ """Response for page search."""
19
+
20
+ pages: List[PageResult] = Field(default_factory=list)
21
+ total: int
22
+
23
+
24
+ # Keyword search models
25
+
26
+
27
+ class KeywordResult(BaseModel):
28
+ """Single keyword with metadata."""
29
+
30
+ keyword: str
31
+ volume: Optional[int] = None
32
+ difficulty: Optional[int] = None # 0-100
33
+ cpc: Optional[float] = None
34
+ intent: Optional[str] = None # informational, commercial, transactional, navigational
35
+
36
+
37
+ class KeywordSearchResponse(BaseModel):
38
+ """Response for keyword search."""
39
+
40
+ keywords: List[KeywordResult] = Field(default_factory=list)
41
+ total: int
42
+
43
+
44
+ # Get page models
45
+
46
+
47
+ class PageLink(BaseModel):
48
+ """Link in page details."""
49
+
50
+ source_url: str
51
+ target_url: str
52
+ anchor_text: Optional[str] = None
53
+ domain_type: str # "internal" or "external"
54
+ source_title: Optional[str] = None
55
+ target_title: Optional[str] = None
56
+
57
+
58
+ class PageLinksDetail(BaseModel):
59
+ """Links for a page."""
60
+
61
+ inlinks: List[PageLink] = Field(default_factory=list)
62
+ outlinks: List[PageLink] = Field(default_factory=list)
63
+
64
+
65
+ class PageTopicInfo(BaseModel):
66
+ """Topic classification for a page."""
67
+
68
+ topics: List[str] = Field(default_factory=list)
69
+ topic_scores: List[float] = Field(default_factory=list)
70
+ page_type: Optional[str] = None # "pillar" or "landing"
71
+ assigned_topic: Optional[str] = None
72
+
73
+
74
+ class PageDetailResponse(BaseModel):
75
+ """Detailed page information."""
76
+
77
+ url: str
78
+ title: Optional[str] = None
79
+ meta_description: Optional[str] = None
80
+ h1: Optional[str] = None
81
+ domain: Optional[str] = None
82
+ links: Optional[PageLinksDetail] = None
83
+ topic_info: Optional[PageTopicInfo] = None
84
+ content_preview: Optional[str] = None
85
+ content_length: int = 0
@@ -0,0 +1,21 @@
1
+ """Pydantic models for SERP API responses."""
2
+
3
+ from typing import List
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SERPResult(BaseModel):
9
+ """Single search result."""
10
+
11
+ title: str
12
+ url: str
13
+ snippet: str
14
+
15
+
16
+ class SERPResponse(BaseModel):
17
+ """Response for SERP fetch."""
18
+
19
+ query: str
20
+ results: List[SERPResult] = Field(default_factory=list)
21
+ total: int
agentberlin/py.typed ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ """Resource classes for Agent Berlin SDK."""
2
+
3
+ from .analytics import AnalyticsResource
4
+ from .brand import BrandResource
5
+ from .keywords import KeywordsResource
6
+ from .pages import PagesResource
7
+ from .serp import SERPResource
8
+
9
+ __all__ = [
10
+ "AnalyticsResource",
11
+ "PagesResource",
12
+ "KeywordsResource",
13
+ "BrandResource",
14
+ "SERPResource",
15
+ ]
@@ -0,0 +1,40 @@
1
+ """Analytics resource for Agent Berlin SDK."""
2
+
3
+ from .._http import HTTPClient
4
+ from ..models.analytics import AnalyticsResponse
5
+
6
+
7
+ class AnalyticsResource:
8
+ """Resource for analytics operations.
9
+
10
+ Example:
11
+ analytics = client.analytics.get(project_domain="example.com")
12
+ print(f"Visibility: {analytics.visibility.current_percentage}%")
13
+ print(f"LLM Sessions: {analytics.traffic.llm_sessions}")
14
+ """
15
+
16
+ def __init__(self, http: HTTPClient) -> None:
17
+ self._http = http
18
+
19
+ def get(self, project_domain: str) -> AnalyticsResponse:
20
+ """Get analytics data for a project.
21
+
22
+ Fetch analytics dashboard data including visibility, traffic,
23
+ topics, and competitors.
24
+
25
+ Args:
26
+ project_domain: The domain of the project (e.g., 'example.com').
27
+
28
+ Returns:
29
+ AnalyticsResponse with comprehensive analytics data.
30
+
31
+ Raises:
32
+ AgentBerlinValidationError: If project_domain is empty.
33
+ AgentBerlinNotFoundError: If the domain doesn't exist.
34
+ AgentBerlinAPIError: If the API returns an error.
35
+ """
36
+ data = self._http.post(
37
+ "/analytics",
38
+ json={"project_domain": project_domain},
39
+ )
40
+ return AnalyticsResponse.model_validate(data)
@@ -0,0 +1,88 @@
1
+ """Brand resource for Agent Berlin SDK."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from .._http import HTTPClient
6
+ from ..models.brand import BrandProfileResponse, BrandProfileUpdateResponse
7
+
8
+
9
+ class BrandResource:
10
+ """Resource for brand profile operations.
11
+
12
+ Example:
13
+ # Get brand profile
14
+ profile = client.brand.get_profile(project_domain="example.com")
15
+ print(f"Domain Authority: {profile.domain_authority}")
16
+ print(f"Competitors: {profile.competitors}")
17
+
18
+ # Update brand profile
19
+ client.brand.update_profile(
20
+ project_domain="example.com",
21
+ field="competitors",
22
+ value="competitor.com",
23
+ mode="add"
24
+ )
25
+ """
26
+
27
+ def __init__(self, http: HTTPClient) -> None:
28
+ self._http = http
29
+
30
+ def get_profile(self, project_domain: str) -> BrandProfileResponse:
31
+ """Get the brand profile configuration for a project.
32
+
33
+ Args:
34
+ project_domain: The domain of the project (e.g., 'example.com').
35
+
36
+ Returns:
37
+ BrandProfileResponse with brand configuration.
38
+ """
39
+ data = self._http.post(
40
+ "/brand/profile",
41
+ json={"project_domain": project_domain},
42
+ )
43
+ return BrandProfileResponse.model_validate(data)
44
+
45
+ def update_profile(
46
+ self,
47
+ project_domain: str,
48
+ field: Literal[
49
+ "name",
50
+ "context",
51
+ "competitors",
52
+ "industries",
53
+ "business_models",
54
+ "company_size",
55
+ "target_segments",
56
+ "geographies",
57
+ "personas",
58
+ ],
59
+ value: str,
60
+ *,
61
+ mode: Literal["add", "set"] = "add",
62
+ ) -> BrandProfileUpdateResponse:
63
+ """Update a specific field in the brand profile.
64
+
65
+ For array fields (competitors, industries, etc.), you can either
66
+ add new values to existing ones or set (replace) all values.
67
+
68
+ Args:
69
+ project_domain: The domain of the project (e.g., 'example.com').
70
+ field: The field to update.
71
+ value: The new value. For array fields, provide comma-separated values.
72
+ mode: For array fields: 'add' adds to existing, 'set' replaces all.
73
+
74
+ Returns:
75
+ BrandProfileUpdateResponse with update confirmation.
76
+
77
+ Note:
78
+ Valid company_size values: solo, early_startup, startup, smb, mid_market, enterprise
79
+ """
80
+ payload: dict[str, object] = {
81
+ "project_domain": project_domain,
82
+ "field": field,
83
+ "value": value,
84
+ "mode": mode,
85
+ }
86
+
87
+ data = self._http.post("/brand/profile/update", json=payload)
88
+ return BrandProfileUpdateResponse.model_validate(data)
@@ -0,0 +1,53 @@
1
+ """Keywords resource for Agent Berlin SDK."""
2
+
3
+ from typing import Optional
4
+
5
+ from .._http import HTTPClient
6
+ from ..models.search import KeywordSearchResponse
7
+
8
+
9
+ class KeywordsResource:
10
+ """Resource for keyword operations.
11
+
12
+ Example:
13
+ results = client.keywords.search(
14
+ project_domain="example.com",
15
+ query="digital marketing",
16
+ limit=20
17
+ )
18
+ for kw in results.keywords:
19
+ print(f"{kw.keyword}: volume={kw.volume}, difficulty={kw.difficulty}")
20
+ """
21
+
22
+ def __init__(self, http: HTTPClient) -> None:
23
+ self._http = http
24
+
25
+ def search(
26
+ self,
27
+ project_domain: str,
28
+ query: str,
29
+ *,
30
+ limit: Optional[int] = None,
31
+ ) -> KeywordSearchResponse:
32
+ """Search for keywords by semantic query.
33
+
34
+ Returns relevant keywords with metadata including search volume,
35
+ difficulty, CPC, and intent classification.
36
+
37
+ Args:
38
+ project_domain: Your project's domain (e.g., 'example.com').
39
+ query: Search query for semantic matching.
40
+ limit: Maximum results (default: 10, max: 50).
41
+
42
+ Returns:
43
+ KeywordSearchResponse with matching keywords and metadata.
44
+ """
45
+ payload: dict[str, object] = {
46
+ "project_domain": project_domain,
47
+ "query": query,
48
+ }
49
+ if limit is not None:
50
+ payload["limit"] = limit
51
+
52
+ data = self._http.post("/keywords/search", json=payload)
53
+ return KeywordSearchResponse.model_validate(data)
@@ -0,0 +1,100 @@
1
+ """Pages resource for Agent Berlin SDK."""
2
+
3
+ from typing import Optional
4
+
5
+ from .._http import HTTPClient
6
+ from ..models.search import PageDetailResponse, PageSearchResponse
7
+
8
+
9
+ class PagesResource:
10
+ """Resource for page operations.
11
+
12
+ Example:
13
+ # Search for pages
14
+ results = client.pages.search(
15
+ project_domain="example.com",
16
+ query="SEO best practices",
17
+ limit=20
18
+ )
19
+
20
+ # Get page details
21
+ page = client.pages.get(
22
+ project_domain="example.com",
23
+ url="https://example.com/blog/seo-tips"
24
+ )
25
+ """
26
+
27
+ def __init__(self, http: HTTPClient) -> None:
28
+ self._http = http
29
+
30
+ def search(
31
+ self,
32
+ project_domain: str,
33
+ query: str,
34
+ *,
35
+ domain: Optional[str] = None,
36
+ limit: Optional[int] = None,
37
+ status_code: Optional[str] = None,
38
+ topic: Optional[str] = None,
39
+ page_type: Optional[str] = None,
40
+ ) -> PageSearchResponse:
41
+ """Search for pages by semantic query.
42
+
43
+ Can search your brand's pages, competitor pages, or all indexed pages.
44
+
45
+ Args:
46
+ project_domain: Your project's domain (e.g., 'example.com').
47
+ query: Search query for semantic matching.
48
+ domain: Optional filter to a specific domain.
49
+ limit: Maximum results (default: 10, max: 50).
50
+ status_code: Filter by HTTP status ('200', '404', 'error', 'redirect', 'success').
51
+ topic: Filter by topic name.
52
+ page_type: Filter by page type ('pillar' or 'landing').
53
+
54
+ Returns:
55
+ PageSearchResponse with matching pages.
56
+ """
57
+ payload: dict[str, object] = {
58
+ "project_domain": project_domain,
59
+ "query": query,
60
+ }
61
+ if domain is not None:
62
+ payload["domain"] = domain
63
+ if limit is not None:
64
+ payload["limit"] = limit
65
+ if status_code is not None:
66
+ payload["status_code"] = status_code
67
+ if topic is not None:
68
+ payload["topic"] = topic
69
+ if page_type is not None:
70
+ payload["page_type"] = page_type
71
+
72
+ data = self._http.post("/pages/search", json=payload)
73
+ return PageSearchResponse.model_validate(data)
74
+
75
+ def get(
76
+ self,
77
+ project_domain: str,
78
+ url: str,
79
+ *,
80
+ content_length: Optional[int] = None,
81
+ ) -> PageDetailResponse:
82
+ """Get detailed information about a specific page.
83
+
84
+ Args:
85
+ project_domain: Your project's domain (e.g., 'example.com').
86
+ url: The page URL to look up.
87
+ content_length: Max characters of content to return (default: 0).
88
+
89
+ Returns:
90
+ PageDetailResponse with page metadata, links, and topic info.
91
+ """
92
+ payload: dict[str, object] = {
93
+ "project_domain": project_domain,
94
+ "url": url,
95
+ }
96
+ if content_length is not None:
97
+ payload["content_length"] = content_length
98
+
99
+ data = self._http.post("/pages/get", json=payload)
100
+ return PageDetailResponse.model_validate(data)
@@ -0,0 +1,57 @@
1
+ """SERP resource for Agent Berlin SDK."""
2
+
3
+ from typing import Optional
4
+
5
+ from .._http import HTTPClient
6
+ from ..models.serp import SERPResponse
7
+
8
+
9
+ class SERPResource:
10
+ """Resource for SERP (Search Engine Results Page) operations.
11
+
12
+ Example:
13
+ results = client.serp.fetch(
14
+ query="best seo tools",
15
+ max_results=5,
16
+ country="us",
17
+ language="lang_en"
18
+ )
19
+ for result in results.results:
20
+ print(f"{result.title}: {result.url}")
21
+ """
22
+
23
+ def __init__(self, http: HTTPClient) -> None:
24
+ self._http = http
25
+
26
+ def fetch(
27
+ self,
28
+ query: str,
29
+ *,
30
+ max_results: Optional[int] = None,
31
+ country: Optional[str] = None,
32
+ language: Optional[str] = None,
33
+ ) -> SERPResponse:
34
+ """Fetch Google SERP data for a query.
35
+
36
+ Returns titles, URLs, and snippets of top-ranking pages.
37
+
38
+ Args:
39
+ query: The search query to execute.
40
+ max_results: Number of results (1-10, default: 10).
41
+ country: ISO 3166-1 alpha-2 country code (e.g., 'us', 'gb', 'de').
42
+ language: ISO 639-1 language code prefixed with 'lang_'
43
+ (e.g., 'lang_en', 'lang_de', 'lang_fr').
44
+
45
+ Returns:
46
+ SERPResponse with search results.
47
+ """
48
+ payload: dict[str, object] = {"query": query}
49
+ if max_results is not None:
50
+ payload["max_results"] = max_results
51
+ if country is not None:
52
+ payload["country"] = country
53
+ if language is not None:
54
+ payload["language"] = language
55
+
56
+ data = self._http.post("/serp/fetch", json=payload)
57
+ return SERPResponse.model_validate(data)
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentberlin
3
+ Version: 0.9.0
4
+ Summary: Python SDK for Agent Berlin - AI-powered SEO and AEO automation
5
+ Project-URL: Homepage, https://agentberlin.ai
6
+ Project-URL: Documentation, https://docs.agentberlin.ai/sdk/python
7
+ Project-URL: Repository, https://github.com/boat-builder/berlin
8
+ Author-email: Agent Berlin <support@agentberlin.ai>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: aeo,ai,analytics,optimization,search,seo
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Requires-Dist: requests>=2.28.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: black>=24.0.0; extra == 'dev'
26
+ Requires-Dist: isort>=5.13.0; extra == 'dev'
27
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
30
+ Requires-Dist: responses>=0.25.0; extra == 'dev'
31
+ Requires-Dist: types-requests>=2.31.0; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Agent Berlin Python SDK
35
+
36
+ Official Python SDK for [Agent Berlin](https://agentberlin.ai) - AI-powered SEO and AEO automation.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install agentberlin
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ from agentberlin import AgentBerlin
48
+
49
+ # Set your API token as an environment variable
50
+ # export AGENTBERLIN_TOKEN="your-token-here"
51
+
52
+ client = AgentBerlin()
53
+
54
+ # Get analytics for your project
55
+ analytics = client.analytics.get(project_domain="example.com")
56
+ print(f"Visibility: {analytics.visibility.current_percentage}%")
57
+ print(f"LLM Sessions: {analytics.traffic.llm_sessions}")
58
+
59
+ # Search for pages
60
+ pages = client.pages.search(
61
+ project_domain="example.com",
62
+ query="SEO best practices",
63
+ limit=10
64
+ )
65
+ for page in pages.pages:
66
+ print(f" - {page.title}: {page.url}")
67
+
68
+ # Search for keywords
69
+ keywords = client.keywords.search(
70
+ project_domain="example.com",
71
+ query="digital marketing"
72
+ )
73
+ for kw in keywords.keywords:
74
+ print(f" - {kw.keyword} (volume: {kw.volume})")
75
+
76
+ # Get page details
77
+ page = client.pages.get(
78
+ project_domain="example.com",
79
+ url="https://example.com/blog/seo-tips"
80
+ )
81
+ print(f"Title: {page.title}")
82
+ print(f"H1: {page.h1}")
83
+
84
+ # Get brand profile
85
+ profile = client.brand.get_profile(project_domain="example.com")
86
+ print(f"Domain Authority: {profile.domain_authority}")
87
+
88
+ # Update brand profile
89
+ client.brand.update_profile(
90
+ project_domain="example.com",
91
+ field="competitors",
92
+ value="competitor.com",
93
+ mode="add"
94
+ )
95
+
96
+ # Fetch SERP results
97
+ serp = client.serp.fetch(query="best seo tools", max_results=5)
98
+ for result in serp.results:
99
+ print(f" - {result.title}: {result.url}")
100
+ ```
101
+
102
+ ## Authentication
103
+
104
+ The SDK requires an API token. Set it as an environment variable:
105
+
106
+ ```bash
107
+ export AGENTBERLIN_TOKEN="your-token-here"
108
+ ```
109
+
110
+ Or pass it directly:
111
+
112
+ ```python
113
+ client = AgentBerlin(token="your-token-here")
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ ```python
119
+ client = AgentBerlin(
120
+ token="your-token", # Optional if AGENTBERLIN_TOKEN env var is set
121
+ base_url="https://...", # Optional, defaults to production API
122
+ timeout=30, # Request timeout in seconds
123
+ )
124
+ ```
125
+
126
+ ## Resources
127
+
128
+ ### Analytics
129
+ ```python
130
+ client.analytics.get(project_domain="example.com")
131
+ ```
132
+
133
+ ### Pages
134
+ ```python
135
+ client.pages.search(project_domain, query, domain=None, limit=10, status_code=None, topic=None, page_type=None)
136
+ client.pages.get(project_domain, url, content_length=0)
137
+ ```
138
+
139
+ ### Keywords
140
+ ```python
141
+ client.keywords.search(project_domain, query, limit=10)
142
+ ```
143
+
144
+ ### Brand
145
+ ```python
146
+ client.brand.get_profile(project_domain)
147
+ client.brand.update_profile(project_domain, field, value, mode="add")
148
+ ```
149
+
150
+ ### SERP
151
+ ```python
152
+ client.serp.fetch(query, max_results=10, country=None, language=None)
153
+ ```
154
+
155
+ ## Error Handling
156
+
157
+ ```python
158
+ from agentberlin import AgentBerlin
159
+ from agentberlin.exceptions import (
160
+ AgentBerlinError,
161
+ AgentBerlinAuthenticationError,
162
+ AgentBerlinNotFoundError,
163
+ AgentBerlinRateLimitError,
164
+ )
165
+
166
+ client = AgentBerlin()
167
+
168
+ try:
169
+ analytics = client.analytics.get(project_domain="example.com")
170
+ except AgentBerlinAuthenticationError:
171
+ print("Invalid or missing API token")
172
+ except AgentBerlinNotFoundError:
173
+ print("Domain not found")
174
+ except AgentBerlinRateLimitError as e:
175
+ print(f"Rate limited. Retry after {e.retry_after} seconds")
176
+ except AgentBerlinError as e:
177
+ print(f"API error: {e.message}")
178
+ ```
179
+
180
+ ## License
181
+
182
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,21 @@
1
+ agentberlin/__init__.py,sha256=MMlz6BOMuAXTErssccHYbSP4AFAcA773hdcs-K83HxM,1327
2
+ agentberlin/_http.py,sha256=_ZoYl5ro23Yub_5HTtS4qX-m0RlhFV2-mF2WLcTUgEA,5995
3
+ agentberlin/client.py,sha256=MPy8MzI5yX7kru2_NI8LEXOOXzaLYZlAUuMmGEly9UE,2880
4
+ agentberlin/config.py,sha256=qPGBExHv1sPY9o9CDrhnbHS9EDRPSn-_yg2zL2H6PNo,408
5
+ agentberlin/exceptions.py,sha256=3YCqboljn9LSi0ueGDBR7h9lGKa0RbLD3LOmhqMZ5RU,2561
6
+ agentberlin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ agentberlin/models/__init__.py,sha256=dumF4EML3WklZjpzYuqA35FeO97pntf8DZUPu6zNGuo,1100
8
+ agentberlin/models/analytics.py,sha256=7Ot--db2VW5b0qnV04O_ZL5jIUlAnLIO_c0EjsePnXI,1849
9
+ agentberlin/models/brand.py,sha256=UD9NdknaPt3vbFs9DhGzvsrw6WykMBJHKsBVllzAW9A,1131
10
+ agentberlin/models/search.py,sha256=vZp-hR32CwDBagyngxw3N3-i7I4Ko-FrKDvxksEHHg8,2042
11
+ agentberlin/models/serp.py,sha256=QyEdtVvzPpqjRtmxuX7sKtlcBnOhUQTsZPS3cwZuI0M,378
12
+ agentberlin/resources/__init__.py,sha256=5_MjO0SyAxpR7QZkP9nYo5-m_ctsjwgAwxWDSYHyojo,349
13
+ agentberlin/resources/analytics.py,sha256=iVcknPm6gZXt920xhlHy8Pk8T-WL-rYpung2qqrvEQs,1292
14
+ agentberlin/resources/brand.py,sha256=tgV-YY2NS82-GRhBiwhgyvu5jgbMszZ1_wEVSXn5eOo,2752
15
+ agentberlin/resources/keywords.py,sha256=cCluTqCplTom1YAqnZV8C5nJpiByQ1-SR-XtcoCV2sA,1548
16
+ agentberlin/resources/pages.py,sha256=jeZu5JXr-rvGSepqoTsowkMQ1UFj5WBGPMckbPnIB0Q,3132
17
+ agentberlin/resources/serp.py,sha256=MZdAIicsMewHVm3k5JkBBow6PipQrrTKjPi1P4sHz8o,1715
18
+ agentberlin-0.9.0.dist-info/METADATA,sha256=7SebC4-n3E9Qe1WOQmpZa_IKJ4k7a3LQh2gQW97W9pE,4766
19
+ agentberlin-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
+ agentberlin-0.9.0.dist-info/licenses/LICENSE,sha256=BX6Yp57z81S2GsPrqvDKIucPL9jG5U3Jaqzn_dxO0wA,1069
21
+ agentberlin-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Agent Berlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.