genxai-framework 0.1.0__py3-none-any.whl → 0.1.2__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.
- cli/commands/__init__.py +3 -1
- cli/commands/connector.py +309 -0
- cli/commands/workflow.py +80 -0
- cli/main.py +3 -1
- genxai/__init__.py +33 -0
- genxai/agents/__init__.py +8 -0
- genxai/agents/presets.py +53 -0
- genxai/connectors/__init__.py +10 -0
- genxai/connectors/base.py +3 -3
- genxai/connectors/config_store.py +106 -0
- genxai/connectors/github.py +117 -0
- genxai/connectors/google_workspace.py +124 -0
- genxai/connectors/jira.py +108 -0
- genxai/connectors/notion.py +97 -0
- genxai/connectors/slack.py +121 -0
- genxai/core/agent/config_io.py +32 -1
- genxai/core/agent/runtime.py +41 -4
- genxai/core/graph/__init__.py +3 -0
- genxai/core/graph/engine.py +218 -11
- genxai/core/graph/executor.py +103 -10
- genxai/core/graph/nodes.py +28 -0
- genxai/core/graph/workflow_io.py +199 -0
- genxai/flows/__init__.py +33 -0
- genxai/flows/auction.py +66 -0
- genxai/flows/base.py +134 -0
- genxai/flows/conditional.py +45 -0
- genxai/flows/coordinator_worker.py +62 -0
- genxai/flows/critic_review.py +62 -0
- genxai/flows/ensemble_voting.py +49 -0
- genxai/flows/loop.py +42 -0
- genxai/flows/map_reduce.py +61 -0
- genxai/flows/p2p.py +146 -0
- genxai/flows/parallel.py +27 -0
- genxai/flows/round_robin.py +24 -0
- genxai/flows/router.py +45 -0
- genxai/flows/selector.py +63 -0
- genxai/flows/subworkflow.py +35 -0
- genxai/llm/factory.py +17 -10
- genxai/llm/providers/anthropic.py +116 -1
- genxai/observability/logging.py +2 -2
- genxai/security/auth.py +10 -6
- genxai/security/cost_control.py +6 -6
- genxai/security/jwt.py +2 -2
- genxai/security/pii.py +2 -2
- genxai/tools/builtin/__init__.py +3 -0
- genxai/tools/builtin/communication/human_input.py +32 -0
- genxai/tools/custom/test-2.py +19 -0
- genxai/tools/custom/test_tool_ui.py +9 -0
- genxai/tools/persistence/service.py +3 -3
- genxai/triggers/schedule.py +2 -2
- genxai/utils/tokens.py +6 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/METADATA +63 -12
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/RECORD +57 -28
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/WHEEL +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/entry_points.txt +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {genxai_framework-0.1.0.dist-info → genxai_framework-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""GitHub connector implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from genxai.connectors.base import Connector
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitHubConnector(Connector):
|
|
17
|
+
"""GitHub connector using REST API v3.
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Provide a personal access token with required scopes.
|
|
21
|
+
- Incoming webhook events can be forwarded to `handle_event`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
connector_id: str,
|
|
27
|
+
token: str,
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
base_url: str = "https://api.github.com",
|
|
30
|
+
timeout: float = 10.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(connector_id=connector_id, name=name)
|
|
33
|
+
self.token = token
|
|
34
|
+
self.base_url = base_url.rstrip("/")
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
37
|
+
self._lock = asyncio.Lock()
|
|
38
|
+
|
|
39
|
+
async def _start(self) -> None:
|
|
40
|
+
if not self._client:
|
|
41
|
+
self._client = httpx.AsyncClient(
|
|
42
|
+
base_url=self.base_url,
|
|
43
|
+
headers={
|
|
44
|
+
"Authorization": f"Bearer {self.token}",
|
|
45
|
+
"Accept": "application/vnd.github+json",
|
|
46
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
47
|
+
},
|
|
48
|
+
timeout=self.timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def _stop(self) -> None:
|
|
52
|
+
if self._client:
|
|
53
|
+
await self._client.aclose()
|
|
54
|
+
self._client = None
|
|
55
|
+
|
|
56
|
+
async def validate_config(self) -> None:
|
|
57
|
+
if not self.token:
|
|
58
|
+
raise ValueError("GitHub token must be provided")
|
|
59
|
+
|
|
60
|
+
async def get_repo(self, owner: str, repo: str) -> Dict[str, Any]:
|
|
61
|
+
"""Fetch repository metadata."""
|
|
62
|
+
return await self._get(f"/repos/{owner}/{repo}")
|
|
63
|
+
|
|
64
|
+
async def list_issues(
|
|
65
|
+
self,
|
|
66
|
+
owner: str,
|
|
67
|
+
repo: str,
|
|
68
|
+
state: str = "open",
|
|
69
|
+
per_page: int = 30,
|
|
70
|
+
) -> Any:
|
|
71
|
+
"""List issues for a repository."""
|
|
72
|
+
return await self._get(
|
|
73
|
+
f"/repos/{owner}/{repo}/issues",
|
|
74
|
+
params={"state": state, "per_page": per_page},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def create_issue(
|
|
78
|
+
self,
|
|
79
|
+
owner: str,
|
|
80
|
+
repo: str,
|
|
81
|
+
title: str,
|
|
82
|
+
body: Optional[str] = None,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""Create a new GitHub issue."""
|
|
85
|
+
payload: Dict[str, Any] = {"title": title}
|
|
86
|
+
if body:
|
|
87
|
+
payload["body"] = body
|
|
88
|
+
return await self._post(f"/repos/{owner}/{repo}/issues", payload)
|
|
89
|
+
|
|
90
|
+
async def handle_event(
|
|
91
|
+
self,
|
|
92
|
+
payload: Dict[str, Any],
|
|
93
|
+
headers: Optional[Dict[str, str]] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Handle an inbound GitHub webhook event and emit it downstream."""
|
|
96
|
+
await self.emit(payload=payload, metadata={"headers": headers or {}})
|
|
97
|
+
|
|
98
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
99
|
+
await self._ensure_client()
|
|
100
|
+
assert self._client is not None
|
|
101
|
+
response = await self._client.get(path, params=params or {})
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
return response.json()
|
|
104
|
+
|
|
105
|
+
async def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
106
|
+
await self._ensure_client()
|
|
107
|
+
assert self._client is not None
|
|
108
|
+
response = await self._client.post(path, json=payload)
|
|
109
|
+
response.raise_for_status()
|
|
110
|
+
return response.json()
|
|
111
|
+
|
|
112
|
+
async def _ensure_client(self) -> None:
|
|
113
|
+
if self._client is not None:
|
|
114
|
+
return
|
|
115
|
+
async with self._lock:
|
|
116
|
+
if self._client is None:
|
|
117
|
+
await self._start()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Google Workspace connector implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from genxai.connectors.base import Connector
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GoogleWorkspaceConnector(Connector):
|
|
17
|
+
"""Google Workspace connector using Google REST APIs.
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Provide an OAuth access token (Bearer) with required scopes.
|
|
21
|
+
- Supports basic operations for Sheets, Drive, and Calendar.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
connector_id: str,
|
|
27
|
+
access_token: str,
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
base_url: str = "https://www.googleapis.com",
|
|
30
|
+
timeout: float = 10.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(connector_id=connector_id, name=name)
|
|
33
|
+
self.access_token = access_token
|
|
34
|
+
self.base_url = base_url.rstrip("/")
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
37
|
+
self._lock = asyncio.Lock()
|
|
38
|
+
|
|
39
|
+
async def _start(self) -> None:
|
|
40
|
+
if not self._client:
|
|
41
|
+
self._client = httpx.AsyncClient(
|
|
42
|
+
base_url=self.base_url,
|
|
43
|
+
headers={
|
|
44
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
45
|
+
"Accept": "application/json",
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
timeout=self.timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def _stop(self) -> None:
|
|
52
|
+
if self._client:
|
|
53
|
+
await self._client.aclose()
|
|
54
|
+
self._client = None
|
|
55
|
+
|
|
56
|
+
async def validate_config(self) -> None:
|
|
57
|
+
if not self.access_token:
|
|
58
|
+
raise ValueError("Google Workspace access_token must be provided")
|
|
59
|
+
|
|
60
|
+
async def get_sheet(self, spreadsheet_id: str) -> Dict[str, Any]:
|
|
61
|
+
"""Fetch spreadsheet metadata."""
|
|
62
|
+
return await self._get(f"/sheets/v4/spreadsheets/{spreadsheet_id}")
|
|
63
|
+
|
|
64
|
+
async def append_sheet_values(
|
|
65
|
+
self,
|
|
66
|
+
spreadsheet_id: str,
|
|
67
|
+
range_: str,
|
|
68
|
+
values: list[list[Any]],
|
|
69
|
+
value_input_option: str = "USER_ENTERED",
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""Append values to a Google Sheet."""
|
|
72
|
+
params = {"valueInputOption": value_input_option}
|
|
73
|
+
payload = {"values": values}
|
|
74
|
+
return await self._post(
|
|
75
|
+
f"/sheets/v4/spreadsheets/{spreadsheet_id}/values/{range_}:append",
|
|
76
|
+
payload,
|
|
77
|
+
params=params,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def list_drive_files(self, page_size: int = 10, query: Optional[str] = None) -> Dict[str, Any]:
|
|
81
|
+
"""List Drive files."""
|
|
82
|
+
params: Dict[str, Any] = {"pageSize": page_size}
|
|
83
|
+
if query:
|
|
84
|
+
params["q"] = query
|
|
85
|
+
return await self._get("/drive/v3/files", params=params)
|
|
86
|
+
|
|
87
|
+
async def get_calendar_events(
|
|
88
|
+
self,
|
|
89
|
+
calendar_id: str = "primary",
|
|
90
|
+
max_results: int = 10,
|
|
91
|
+
) -> Dict[str, Any]:
|
|
92
|
+
"""List calendar events."""
|
|
93
|
+
params = {"maxResults": max_results, "singleEvents": True, "orderBy": "startTime"}
|
|
94
|
+
return await self._get(f"/calendar/v3/calendars/{calendar_id}/events", params=params)
|
|
95
|
+
|
|
96
|
+
async def handle_event(self, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> None:
|
|
97
|
+
"""Handle an inbound event payload and emit it downstream."""
|
|
98
|
+
await self.emit(payload=payload, metadata={"headers": headers or {}})
|
|
99
|
+
|
|
100
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
101
|
+
await self._ensure_client()
|
|
102
|
+
assert self._client is not None
|
|
103
|
+
response = await self._client.get(path, params=params or {})
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
return response.json()
|
|
106
|
+
|
|
107
|
+
async def _post(
|
|
108
|
+
self,
|
|
109
|
+
path: str,
|
|
110
|
+
payload: Dict[str, Any],
|
|
111
|
+
params: Optional[Dict[str, Any]] = None,
|
|
112
|
+
) -> Dict[str, Any]:
|
|
113
|
+
await self._ensure_client()
|
|
114
|
+
assert self._client is not None
|
|
115
|
+
response = await self._client.post(path, params=params or {}, json=payload)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
return response.json()
|
|
118
|
+
|
|
119
|
+
async def _ensure_client(self) -> None:
|
|
120
|
+
if self._client is not None:
|
|
121
|
+
return
|
|
122
|
+
async with self._lock:
|
|
123
|
+
if self._client is None:
|
|
124
|
+
await self._start()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Jira connector implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import asyncio
|
|
7
|
+
import base64
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from genxai.connectors.base import Connector
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JiraConnector(Connector):
|
|
18
|
+
"""Jira connector using Jira Cloud REST API v3.
|
|
19
|
+
|
|
20
|
+
Notes:
|
|
21
|
+
- Use email + API token for basic auth.
|
|
22
|
+
- Incoming webhook events can be forwarded to `handle_event`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
connector_id: str,
|
|
28
|
+
email: str,
|
|
29
|
+
api_token: str,
|
|
30
|
+
base_url: str,
|
|
31
|
+
name: Optional[str] = None,
|
|
32
|
+
timeout: float = 10.0,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(connector_id=connector_id, name=name)
|
|
35
|
+
self.email = email
|
|
36
|
+
self.api_token = api_token
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
self.timeout = timeout
|
|
39
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
40
|
+
self._lock = asyncio.Lock()
|
|
41
|
+
|
|
42
|
+
async def _start(self) -> None:
|
|
43
|
+
if not self._client:
|
|
44
|
+
token = f"{self.email}:{self.api_token}".encode("utf-8")
|
|
45
|
+
auth_header = base64.b64encode(token).decode("utf-8")
|
|
46
|
+
self._client = httpx.AsyncClient(
|
|
47
|
+
base_url=self.base_url,
|
|
48
|
+
headers={
|
|
49
|
+
"Authorization": f"Basic {auth_header}",
|
|
50
|
+
"Accept": "application/json",
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
},
|
|
53
|
+
timeout=self.timeout,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def _stop(self) -> None:
|
|
57
|
+
if self._client:
|
|
58
|
+
await self._client.aclose()
|
|
59
|
+
self._client = None
|
|
60
|
+
|
|
61
|
+
async def validate_config(self) -> None:
|
|
62
|
+
if not self.email:
|
|
63
|
+
raise ValueError("Jira email must be provided")
|
|
64
|
+
if not self.api_token:
|
|
65
|
+
raise ValueError("Jira api_token must be provided")
|
|
66
|
+
if not self.base_url:
|
|
67
|
+
raise ValueError("Jira base_url must be provided")
|
|
68
|
+
|
|
69
|
+
async def get_project(self, project_key: str) -> Dict[str, Any]:
|
|
70
|
+
"""Fetch Jira project metadata."""
|
|
71
|
+
return await self._get(f"/rest/api/3/project/{project_key}")
|
|
72
|
+
|
|
73
|
+
async def search_issues(self, jql: str, max_results: int = 50) -> Dict[str, Any]:
|
|
74
|
+
"""Search issues with JQL."""
|
|
75
|
+
payload = {"jql": jql, "maxResults": max_results}
|
|
76
|
+
return await self._post("/rest/api/3/search", payload)
|
|
77
|
+
|
|
78
|
+
async def create_issue(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
79
|
+
"""Create a Jira issue using the provided payload."""
|
|
80
|
+
return await self._post("/rest/api/3/issue", payload)
|
|
81
|
+
|
|
82
|
+
async def handle_event(
|
|
83
|
+
self,
|
|
84
|
+
payload: Dict[str, Any],
|
|
85
|
+
headers: Optional[Dict[str, str]] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
await self.emit(payload=payload, metadata={"headers": headers or {}})
|
|
88
|
+
|
|
89
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
90
|
+
await self._ensure_client()
|
|
91
|
+
assert self._client is not None
|
|
92
|
+
response = await self._client.get(path, params=params or {})
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
return response.json()
|
|
95
|
+
|
|
96
|
+
async def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
97
|
+
await self._ensure_client()
|
|
98
|
+
assert self._client is not None
|
|
99
|
+
response = await self._client.post(path, json=payload)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
return response.json()
|
|
102
|
+
|
|
103
|
+
async def _ensure_client(self) -> None:
|
|
104
|
+
if self._client is not None:
|
|
105
|
+
return
|
|
106
|
+
async with self._lock:
|
|
107
|
+
if self._client is None:
|
|
108
|
+
await self._start()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Notion connector implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from genxai.connectors.base import Connector
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotionConnector(Connector):
|
|
17
|
+
"""Notion connector using the Notion API.
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Provide a Notion integration token.
|
|
21
|
+
- Incoming webhook-like events can be forwarded to `handle_event`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
connector_id: str,
|
|
27
|
+
token: str,
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
base_url: str = "https://api.notion.com/v1",
|
|
30
|
+
notion_version: str = "2022-06-28",
|
|
31
|
+
timeout: float = 10.0,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(connector_id=connector_id, name=name)
|
|
34
|
+
self.token = token
|
|
35
|
+
self.base_url = base_url.rstrip("/")
|
|
36
|
+
self.notion_version = notion_version
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
39
|
+
self._lock = asyncio.Lock()
|
|
40
|
+
|
|
41
|
+
async def _start(self) -> None:
|
|
42
|
+
if not self._client:
|
|
43
|
+
self._client = httpx.AsyncClient(
|
|
44
|
+
base_url=self.base_url,
|
|
45
|
+
headers={
|
|
46
|
+
"Authorization": f"Bearer {self.token}",
|
|
47
|
+
"Notion-Version": self.notion_version,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
timeout=self.timeout,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def _stop(self) -> None:
|
|
54
|
+
if self._client:
|
|
55
|
+
await self._client.aclose()
|
|
56
|
+
self._client = None
|
|
57
|
+
|
|
58
|
+
async def validate_config(self) -> None:
|
|
59
|
+
if not self.token:
|
|
60
|
+
raise ValueError("Notion token must be provided")
|
|
61
|
+
|
|
62
|
+
async def get_page(self, page_id: str) -> Dict[str, Any]:
|
|
63
|
+
"""Fetch a Notion page by ID."""
|
|
64
|
+
return await self._get(f"/pages/{page_id}")
|
|
65
|
+
|
|
66
|
+
async def query_database(self, database_id: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
67
|
+
"""Query a Notion database."""
|
|
68
|
+
return await self._post(f"/databases/{database_id}/query", payload or {})
|
|
69
|
+
|
|
70
|
+
async def create_page(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
"""Create a Notion page using the provided payload."""
|
|
72
|
+
return await self._post("/pages", payload)
|
|
73
|
+
|
|
74
|
+
async def handle_event(self, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> None:
|
|
75
|
+
"""Handle an inbound Notion event payload and emit it downstream."""
|
|
76
|
+
await self.emit(payload=payload, metadata={"headers": headers or {}})
|
|
77
|
+
|
|
78
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
79
|
+
await self._ensure_client()
|
|
80
|
+
assert self._client is not None
|
|
81
|
+
response = await self._client.get(path, params=params or {})
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return response.json()
|
|
84
|
+
|
|
85
|
+
async def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
86
|
+
await self._ensure_client()
|
|
87
|
+
assert self._client is not None
|
|
88
|
+
response = await self._client.post(path, json=payload)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
async def _ensure_client(self) -> None:
|
|
93
|
+
if self._client is not None:
|
|
94
|
+
return
|
|
95
|
+
async with self._lock:
|
|
96
|
+
if self._client is None:
|
|
97
|
+
await self._start()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Slack connector implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from genxai.connectors.base import Connector
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SlackConnector(Connector):
|
|
17
|
+
"""Slack connector using Slack Web API + Events API payloads.
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- This connector handles outgoing API calls and can emit inbound events
|
|
21
|
+
when `handle_event` is called by your webhook route.
|
|
22
|
+
- You must provide a Slack Bot token for Web API calls.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
connector_id: str,
|
|
28
|
+
bot_token: str,
|
|
29
|
+
name: Optional[str] = None,
|
|
30
|
+
base_url: str = "https://slack.com/api",
|
|
31
|
+
timeout: float = 10.0,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(connector_id=connector_id, name=name)
|
|
34
|
+
self.bot_token = bot_token
|
|
35
|
+
self.base_url = base_url.rstrip("/")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
38
|
+
self._lock = asyncio.Lock()
|
|
39
|
+
|
|
40
|
+
async def _start(self) -> None:
|
|
41
|
+
if not self._client:
|
|
42
|
+
self._client = httpx.AsyncClient(
|
|
43
|
+
base_url=self.base_url,
|
|
44
|
+
headers={
|
|
45
|
+
"Authorization": f"Bearer {self.bot_token}",
|
|
46
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
47
|
+
},
|
|
48
|
+
timeout=self.timeout,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def _stop(self) -> None:
|
|
52
|
+
if self._client:
|
|
53
|
+
await self._client.aclose()
|
|
54
|
+
self._client = None
|
|
55
|
+
|
|
56
|
+
async def validate_config(self) -> None:
|
|
57
|
+
if not self.bot_token:
|
|
58
|
+
raise ValueError("Slack bot_token must be provided")
|
|
59
|
+
|
|
60
|
+
async def send_message(
|
|
61
|
+
self,
|
|
62
|
+
channel: str,
|
|
63
|
+
text: str,
|
|
64
|
+
blocks: Optional[List[Dict[str, Any]]] = None,
|
|
65
|
+
attachments: Optional[List[Dict[str, Any]]] = None,
|
|
66
|
+
) -> Dict[str, Any]:
|
|
67
|
+
"""Send a message to a Slack channel."""
|
|
68
|
+
payload: Dict[str, Any] = {"channel": channel, "text": text}
|
|
69
|
+
if blocks:
|
|
70
|
+
payload["blocks"] = blocks
|
|
71
|
+
if attachments:
|
|
72
|
+
payload["attachments"] = attachments
|
|
73
|
+
return await self._post("/chat.postMessage", payload)
|
|
74
|
+
|
|
75
|
+
async def post_ephemeral(
|
|
76
|
+
self,
|
|
77
|
+
channel: str,
|
|
78
|
+
user: str,
|
|
79
|
+
text: str,
|
|
80
|
+
blocks: Optional[List[Dict[str, Any]]] = None,
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Send an ephemeral message to a user in a channel."""
|
|
83
|
+
payload: Dict[str, Any] = {"channel": channel, "user": user, "text": text}
|
|
84
|
+
if blocks:
|
|
85
|
+
payload["blocks"] = blocks
|
|
86
|
+
return await self._post("/chat.postEphemeral", payload)
|
|
87
|
+
|
|
88
|
+
async def list_channels(self, types: str = "public_channel,private_channel") -> Dict[str, Any]:
|
|
89
|
+
"""List available channels."""
|
|
90
|
+
return await self._get("/conversations.list", {"types": types})
|
|
91
|
+
|
|
92
|
+
async def handle_event(self, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> None:
|
|
93
|
+
"""Handle an inbound Slack event payload and emit it downstream."""
|
|
94
|
+
await self.emit(payload=payload, metadata={"headers": headers or {}})
|
|
95
|
+
|
|
96
|
+
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
97
|
+
await self._ensure_client()
|
|
98
|
+
assert self._client is not None
|
|
99
|
+
response = await self._client.get(path, params=params or {})
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
data = response.json()
|
|
102
|
+
if not data.get("ok", False):
|
|
103
|
+
raise ValueError(f"Slack API error: {data}")
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
async def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
107
|
+
await self._ensure_client()
|
|
108
|
+
assert self._client is not None
|
|
109
|
+
response = await self._client.post(path, json=payload)
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
data = response.json()
|
|
112
|
+
if not data.get("ok", False):
|
|
113
|
+
raise ValueError(f"Slack API error: {data}")
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
async def _ensure_client(self) -> None:
|
|
117
|
+
if self._client is not None:
|
|
118
|
+
return
|
|
119
|
+
async with self._lock:
|
|
120
|
+
if self._client is None:
|
|
121
|
+
await self._start()
|
genxai/core/agent/config_io.py
CHANGED
|
@@ -26,7 +26,38 @@ def agent_to_dict(agent: Agent) -> Dict[str, Any]:
|
|
|
26
26
|
|
|
27
27
|
def agent_from_dict(data: Dict[str, Any]) -> Agent:
|
|
28
28
|
"""Load Agent from a dictionary."""
|
|
29
|
-
|
|
29
|
+
if "config" in data and isinstance(data.get("config"), dict):
|
|
30
|
+
config = agent_config_from_dict(data["config"])
|
|
31
|
+
return Agent(id=data["id"], config=config)
|
|
32
|
+
|
|
33
|
+
# Support flat agent definitions (no config wrapper).
|
|
34
|
+
llm_model = data.get("llm_model") or data.get("llm") or "gpt-4"
|
|
35
|
+
config = AgentConfig(
|
|
36
|
+
role=data.get("role", "Agent"),
|
|
37
|
+
goal=data.get("goal", "Process tasks"),
|
|
38
|
+
backstory=data.get("backstory", ""),
|
|
39
|
+
llm_provider=data.get("llm_provider", "openai"),
|
|
40
|
+
llm_model=llm_model,
|
|
41
|
+
llm_temperature=data.get("llm_temperature", 0.7),
|
|
42
|
+
tools=data.get("tools", []),
|
|
43
|
+
enable_memory=data.get("memory", {}).get("enabled", True)
|
|
44
|
+
if isinstance(data.get("memory"), dict)
|
|
45
|
+
else data.get("enable_memory", True),
|
|
46
|
+
memory_type=data.get("memory", {}).get("type", "short_term")
|
|
47
|
+
if isinstance(data.get("memory"), dict)
|
|
48
|
+
else data.get("memory_type", "short_term"),
|
|
49
|
+
agent_type=data.get("behavior", {}).get("agent_type", "reactive")
|
|
50
|
+
if isinstance(data.get("behavior"), dict)
|
|
51
|
+
else data.get("agent_type", "reactive"),
|
|
52
|
+
max_iterations=data.get("behavior", {}).get("max_iterations", 10)
|
|
53
|
+
if isinstance(data.get("behavior"), dict)
|
|
54
|
+
else data.get("max_iterations", 10),
|
|
55
|
+
verbose=data.get("behavior", {}).get("verbose", False)
|
|
56
|
+
if isinstance(data.get("behavior"), dict)
|
|
57
|
+
else data.get("verbose", False),
|
|
58
|
+
metadata=data.get("metadata", {}),
|
|
59
|
+
)
|
|
60
|
+
return Agent(id=data["id"], config=config)
|
|
30
61
|
|
|
31
62
|
|
|
32
63
|
def export_agent_config_yaml(agent: Agent, path: Path) -> None:
|