agentmesh-platform 1.0.0a1__py3-none-any.whl → 1.0.0a2__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.
- agentmesh/__init__.py +6 -13
- agentmesh/cli/main.py +131 -0
- agentmesh/cli/proxy.py +448 -0
- agentmesh/core/__init__.py +7 -0
- agentmesh/core/identity/__init__.py +17 -0
- agentmesh/core/identity/ca.py +386 -0
- agentmesh/governance/policy.py +14 -11
- agentmesh/observability/__init__.py +16 -0
- agentmesh/observability/metrics.py +237 -0
- agentmesh/observability/tracing.py +203 -0
- agentmesh/services/__init__.py +10 -0
- agentmesh/services/audit/__init__.py +14 -0
- agentmesh/services/registry/__init__.py +12 -0
- agentmesh/services/registry/agent_registry.py +249 -0
- agentmesh/services/reward_engine/__init__.py +14 -0
- agentmesh/storage/__init__.py +18 -0
- agentmesh/storage/memory_provider.py +232 -0
- agentmesh/storage/postgres_provider.py +463 -0
- agentmesh/storage/provider.py +231 -0
- agentmesh/storage/redis_provider.py +223 -0
- agentmesh/trust/__init__.py +2 -1
- agentmesh/trust/bridge.py +37 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/METADATA +132 -6
- agentmesh_platform-1.0.0a2.dist-info/RECORD +45 -0
- agentmesh_platform-1.0.0a1.dist-info/RECORD +0 -28
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/WHEEL +0 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/entry_points.txt +0 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-Memory Storage Provider.
|
|
3
|
+
|
|
4
|
+
Simple in-memory implementation for development and testing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import asyncio
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
from .provider import AbstractStorageProvider, StorageConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryStorageProvider(AbstractStorageProvider):
|
|
15
|
+
"""
|
|
16
|
+
In-memory storage provider.
|
|
17
|
+
|
|
18
|
+
Uses Python dictionaries for storage. Data is lost on restart.
|
|
19
|
+
Suitable for development and testing only.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: StorageConfig):
|
|
23
|
+
"""Initialize in-memory storage."""
|
|
24
|
+
super().__init__(config)
|
|
25
|
+
self._data: dict[str, str] = {}
|
|
26
|
+
self._hashes: dict[str, dict[str, str]] = defaultdict(dict)
|
|
27
|
+
self._lists: dict[str, list[str]] = defaultdict(list)
|
|
28
|
+
self._sorted_sets: dict[str, dict[str, float]] = defaultdict(dict)
|
|
29
|
+
self._ttls: dict[str, float] = {}
|
|
30
|
+
self._connected = False
|
|
31
|
+
|
|
32
|
+
async def connect(self) -> None:
|
|
33
|
+
"""Establish connection (no-op for memory)."""
|
|
34
|
+
self._connected = True
|
|
35
|
+
|
|
36
|
+
async def disconnect(self) -> None:
|
|
37
|
+
"""Close connection (no-op for memory)."""
|
|
38
|
+
self._connected = False
|
|
39
|
+
|
|
40
|
+
async def health_check(self) -> bool:
|
|
41
|
+
"""Check if storage is healthy."""
|
|
42
|
+
return self._connected
|
|
43
|
+
|
|
44
|
+
# Key-Value Operations
|
|
45
|
+
|
|
46
|
+
async def get(self, key: str) -> Optional[str]:
|
|
47
|
+
"""Get value by key."""
|
|
48
|
+
return self._data.get(key)
|
|
49
|
+
|
|
50
|
+
async def set(
|
|
51
|
+
self,
|
|
52
|
+
key: str,
|
|
53
|
+
value: str,
|
|
54
|
+
ttl_seconds: Optional[int] = None,
|
|
55
|
+
) -> bool:
|
|
56
|
+
"""Set value with optional TTL."""
|
|
57
|
+
self._data[key] = value
|
|
58
|
+
if ttl_seconds is not None:
|
|
59
|
+
import time
|
|
60
|
+
self._ttls[key] = time.time() + ttl_seconds
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
async def delete(self, key: str) -> bool:
|
|
64
|
+
"""Delete key."""
|
|
65
|
+
if key in self._data:
|
|
66
|
+
del self._data[key]
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
async def exists(self, key: str) -> bool:
|
|
71
|
+
"""Check if key exists."""
|
|
72
|
+
return key in self._data
|
|
73
|
+
|
|
74
|
+
# Hash Operations
|
|
75
|
+
|
|
76
|
+
async def hget(self, key: str, field: str) -> Optional[str]:
|
|
77
|
+
"""Get hash field value."""
|
|
78
|
+
return self._hashes.get(key, {}).get(field)
|
|
79
|
+
|
|
80
|
+
async def hset(self, key: str, field: str, value: str) -> bool:
|
|
81
|
+
"""Set hash field value."""
|
|
82
|
+
self._hashes[key][field] = value
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
async def hgetall(self, key: str) -> dict[str, str]:
|
|
86
|
+
"""Get all hash fields."""
|
|
87
|
+
return dict(self._hashes.get(key, {}))
|
|
88
|
+
|
|
89
|
+
async def hdel(self, key: str, field: str) -> bool:
|
|
90
|
+
"""Delete hash field."""
|
|
91
|
+
if key in self._hashes and field in self._hashes[key]:
|
|
92
|
+
del self._hashes[key][field]
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
async def hkeys(self, key: str) -> list[str]:
|
|
97
|
+
"""Get all hash field names."""
|
|
98
|
+
return list(self._hashes.get(key, {}).keys())
|
|
99
|
+
|
|
100
|
+
# List Operations
|
|
101
|
+
|
|
102
|
+
async def lpush(self, key: str, value: str) -> int:
|
|
103
|
+
"""Push value to head of list."""
|
|
104
|
+
self._lists[key].insert(0, value)
|
|
105
|
+
return len(self._lists[key])
|
|
106
|
+
|
|
107
|
+
async def rpush(self, key: str, value: str) -> int:
|
|
108
|
+
"""Push value to tail of list."""
|
|
109
|
+
self._lists[key].append(value)
|
|
110
|
+
return len(self._lists[key])
|
|
111
|
+
|
|
112
|
+
async def lrange(self, key: str, start: int, stop: int) -> list[str]:
|
|
113
|
+
"""Get list range [start, stop]."""
|
|
114
|
+
lst = self._lists.get(key, [])
|
|
115
|
+
if stop == -1:
|
|
116
|
+
return lst[start:]
|
|
117
|
+
return lst[start:stop + 1]
|
|
118
|
+
|
|
119
|
+
async def llen(self, key: str) -> int:
|
|
120
|
+
"""Get list length."""
|
|
121
|
+
return len(self._lists.get(key, []))
|
|
122
|
+
|
|
123
|
+
# Sorted Set Operations
|
|
124
|
+
|
|
125
|
+
async def zadd(self, key: str, score: float, member: str) -> bool:
|
|
126
|
+
"""Add member to sorted set with score."""
|
|
127
|
+
self._sorted_sets[key][member] = score
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
async def zscore(self, key: str, member: str) -> Optional[float]:
|
|
131
|
+
"""Get score of member in sorted set."""
|
|
132
|
+
return self._sorted_sets.get(key, {}).get(member)
|
|
133
|
+
|
|
134
|
+
async def zrange(
|
|
135
|
+
self,
|
|
136
|
+
key: str,
|
|
137
|
+
start: int,
|
|
138
|
+
stop: int,
|
|
139
|
+
with_scores: bool = False,
|
|
140
|
+
) -> list[str] | list[tuple[str, float]]:
|
|
141
|
+
"""Get sorted set range."""
|
|
142
|
+
sorted_set = self._sorted_sets.get(key, {})
|
|
143
|
+
sorted_items = sorted(sorted_set.items(), key=lambda x: x[1])
|
|
144
|
+
|
|
145
|
+
if stop == -1:
|
|
146
|
+
items = sorted_items[start:]
|
|
147
|
+
else:
|
|
148
|
+
items = sorted_items[start:stop + 1]
|
|
149
|
+
|
|
150
|
+
if with_scores:
|
|
151
|
+
return items
|
|
152
|
+
return [member for member, _ in items]
|
|
153
|
+
|
|
154
|
+
async def zrangebyscore(
|
|
155
|
+
self,
|
|
156
|
+
key: str,
|
|
157
|
+
min_score: float,
|
|
158
|
+
max_score: float,
|
|
159
|
+
with_scores: bool = False,
|
|
160
|
+
) -> list[str] | list[tuple[str, float]]:
|
|
161
|
+
"""Get sorted set range by score."""
|
|
162
|
+
sorted_set = self._sorted_sets.get(key, {})
|
|
163
|
+
items = [
|
|
164
|
+
(member, score)
|
|
165
|
+
for member, score in sorted(sorted_set.items(), key=lambda x: x[1])
|
|
166
|
+
if min_score <= score <= max_score
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
if with_scores:
|
|
170
|
+
return items
|
|
171
|
+
return [member for member, _ in items]
|
|
172
|
+
|
|
173
|
+
# Atomic Operations
|
|
174
|
+
|
|
175
|
+
async def incr(self, key: str) -> int:
|
|
176
|
+
"""Increment value atomically."""
|
|
177
|
+
current = int(self._data.get(key, "0"))
|
|
178
|
+
new_value = current + 1
|
|
179
|
+
self._data[key] = str(new_value)
|
|
180
|
+
return new_value
|
|
181
|
+
|
|
182
|
+
async def decr(self, key: str) -> int:
|
|
183
|
+
"""Decrement value atomically."""
|
|
184
|
+
current = int(self._data.get(key, "0"))
|
|
185
|
+
new_value = current - 1
|
|
186
|
+
self._data[key] = str(new_value)
|
|
187
|
+
return new_value
|
|
188
|
+
|
|
189
|
+
async def incrby(self, key: str, amount: int) -> int:
|
|
190
|
+
"""Increment value by amount."""
|
|
191
|
+
current = int(self._data.get(key, "0"))
|
|
192
|
+
new_value = current + amount
|
|
193
|
+
self._data[key] = str(new_value)
|
|
194
|
+
return new_value
|
|
195
|
+
|
|
196
|
+
# Batch Operations
|
|
197
|
+
|
|
198
|
+
async def mget(self, keys: list[str]) -> list[Optional[str]]:
|
|
199
|
+
"""Get multiple values."""
|
|
200
|
+
return [self._data.get(key) for key in keys]
|
|
201
|
+
|
|
202
|
+
async def mset(self, mapping: dict[str, str]) -> bool:
|
|
203
|
+
"""Set multiple key-value pairs."""
|
|
204
|
+
self._data.update(mapping)
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
# Pattern Operations
|
|
208
|
+
|
|
209
|
+
async def keys(self, pattern: str) -> list[str]:
|
|
210
|
+
"""Get keys matching pattern."""
|
|
211
|
+
import fnmatch
|
|
212
|
+
return [key for key in self._data.keys() if fnmatch.fnmatch(key, pattern)]
|
|
213
|
+
|
|
214
|
+
async def scan(
|
|
215
|
+
self,
|
|
216
|
+
cursor: int = 0,
|
|
217
|
+
match: Optional[str] = None,
|
|
218
|
+
count: int = 100,
|
|
219
|
+
) -> tuple[int, list[str]]:
|
|
220
|
+
"""Scan keys with cursor."""
|
|
221
|
+
all_keys = list(self._data.keys())
|
|
222
|
+
|
|
223
|
+
if match:
|
|
224
|
+
import fnmatch
|
|
225
|
+
all_keys = [key for key in all_keys if fnmatch.fnmatch(key, match)]
|
|
226
|
+
|
|
227
|
+
start = cursor
|
|
228
|
+
end = cursor + count
|
|
229
|
+
keys = all_keys[start:end]
|
|
230
|
+
|
|
231
|
+
new_cursor = end if end < len(all_keys) else 0
|
|
232
|
+
return new_cursor, keys
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL Storage Provider.
|
|
3
|
+
|
|
4
|
+
Enterprise-grade PostgreSQL backend with async SQLAlchemy ORM.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from .provider import AbstractStorageProvider, StorageConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PostgresStorageProvider(AbstractStorageProvider):
|
|
14
|
+
"""
|
|
15
|
+
PostgreSQL storage provider.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Async SQLAlchemy ORM
|
|
19
|
+
- Connection pooling
|
|
20
|
+
- JSONB support for structured data
|
|
21
|
+
- Full ACID compliance
|
|
22
|
+
|
|
23
|
+
Requires: sqlalchemy[asyncio], asyncpg packages
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: StorageConfig):
|
|
27
|
+
"""Initialize PostgreSQL storage."""
|
|
28
|
+
super().__init__(config)
|
|
29
|
+
self._engine = None
|
|
30
|
+
self._session_factory = None
|
|
31
|
+
|
|
32
|
+
async def connect(self) -> None:
|
|
33
|
+
"""Establish connection to PostgreSQL."""
|
|
34
|
+
try:
|
|
35
|
+
from sqlalchemy.ext.asyncio import (
|
|
36
|
+
create_async_engine,
|
|
37
|
+
async_sessionmaker,
|
|
38
|
+
)
|
|
39
|
+
except ImportError:
|
|
40
|
+
raise ImportError(
|
|
41
|
+
"sqlalchemy[asyncio] and asyncpg packages are required for PostgresStorageProvider. "
|
|
42
|
+
"Install with: pip install sqlalchemy[asyncio] asyncpg"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Build connection string
|
|
46
|
+
if self.config.connection_string:
|
|
47
|
+
conn_str = self.config.connection_string
|
|
48
|
+
else:
|
|
49
|
+
password_part = (
|
|
50
|
+
f":{self.config.postgres_password}"
|
|
51
|
+
if self.config.postgres_password
|
|
52
|
+
else ""
|
|
53
|
+
)
|
|
54
|
+
conn_str = (
|
|
55
|
+
f"postgresql+asyncpg://{self.config.postgres_user}"
|
|
56
|
+
f"{password_part}@{self.config.postgres_host}"
|
|
57
|
+
f":{self.config.postgres_port}/{self.config.postgres_database}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if self.config.postgres_ssl_mode != "disable":
|
|
61
|
+
conn_str += f"?ssl={self.config.postgres_ssl_mode}"
|
|
62
|
+
|
|
63
|
+
# Create engine
|
|
64
|
+
self._engine = create_async_engine(
|
|
65
|
+
conn_str,
|
|
66
|
+
pool_size=self.config.pool_size,
|
|
67
|
+
max_overflow=20,
|
|
68
|
+
pool_pre_ping=True,
|
|
69
|
+
echo=False,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self._session_factory = async_sessionmaker(
|
|
73
|
+
self._engine,
|
|
74
|
+
expire_on_commit=False,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Initialize schema
|
|
78
|
+
await self._init_schema()
|
|
79
|
+
|
|
80
|
+
async def _init_schema(self) -> None:
|
|
81
|
+
"""Initialize database schema."""
|
|
82
|
+
from sqlalchemy import text
|
|
83
|
+
|
|
84
|
+
# Create tables for key-value, hashes, lists, etc.
|
|
85
|
+
async with self._engine.begin() as conn:
|
|
86
|
+
await conn.execute(text(
|
|
87
|
+
"""
|
|
88
|
+
CREATE TABLE IF NOT EXISTS agentmesh_kv (
|
|
89
|
+
key VARCHAR(512) PRIMARY KEY,
|
|
90
|
+
value TEXT NOT NULL,
|
|
91
|
+
expires_at TIMESTAMP
|
|
92
|
+
);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_kv_expires ON agentmesh_kv(expires_at);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS agentmesh_hash (
|
|
96
|
+
key VARCHAR(512) NOT NULL,
|
|
97
|
+
field VARCHAR(512) NOT NULL,
|
|
98
|
+
value TEXT NOT NULL,
|
|
99
|
+
PRIMARY KEY (key, field)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS agentmesh_list (
|
|
103
|
+
key VARCHAR(512) NOT NULL,
|
|
104
|
+
idx INTEGER NOT NULL,
|
|
105
|
+
value TEXT NOT NULL,
|
|
106
|
+
PRIMARY KEY (key, idx)
|
|
107
|
+
);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_list_key ON agentmesh_list(key, idx);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS agentmesh_zset (
|
|
111
|
+
key VARCHAR(512) NOT NULL,
|
|
112
|
+
member VARCHAR(512) NOT NULL,
|
|
113
|
+
score DOUBLE PRECISION NOT NULL,
|
|
114
|
+
PRIMARY KEY (key, member)
|
|
115
|
+
);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_zset_score ON agentmesh_zset(key, score);
|
|
117
|
+
"""
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
async def disconnect(self) -> None:
|
|
121
|
+
"""Close connection to PostgreSQL."""
|
|
122
|
+
if self._engine:
|
|
123
|
+
await self._engine.dispose()
|
|
124
|
+
|
|
125
|
+
async def health_check(self) -> bool:
|
|
126
|
+
"""Check if PostgreSQL is healthy."""
|
|
127
|
+
try:
|
|
128
|
+
if self._engine:
|
|
129
|
+
from sqlalchemy import text
|
|
130
|
+
async with self._engine.begin() as conn:
|
|
131
|
+
await conn.execute(text("SELECT 1"))
|
|
132
|
+
return True
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
# Key-Value Operations
|
|
138
|
+
|
|
139
|
+
async def get(self, key: str) -> Optional[str]:
|
|
140
|
+
"""Get value by key."""
|
|
141
|
+
async with self._session_factory() as session:
|
|
142
|
+
result = await session.execute(
|
|
143
|
+
"SELECT value FROM agentmesh_kv WHERE key = :key "
|
|
144
|
+
"AND (expires_at IS NULL OR expires_at > NOW())",
|
|
145
|
+
{"key": key},
|
|
146
|
+
)
|
|
147
|
+
row = result.fetchone()
|
|
148
|
+
return row[0] if row else None
|
|
149
|
+
|
|
150
|
+
async def set(
|
|
151
|
+
self,
|
|
152
|
+
key: str,
|
|
153
|
+
value: str,
|
|
154
|
+
ttl_seconds: Optional[int] = None,
|
|
155
|
+
) -> bool:
|
|
156
|
+
"""Set value with optional TTL."""
|
|
157
|
+
async with self._session_factory() as session:
|
|
158
|
+
if ttl_seconds:
|
|
159
|
+
await session.execute(
|
|
160
|
+
"INSERT INTO agentmesh_kv (key, value, expires_at) "
|
|
161
|
+
"VALUES (:key, :value, NOW() + INTERVAL '1 second' * :ttl) "
|
|
162
|
+
"ON CONFLICT (key) DO UPDATE SET value = :value, expires_at = NOW() + INTERVAL '1 second' * :ttl",
|
|
163
|
+
{"key": key, "value": value, "ttl": ttl_seconds},
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
await session.execute(
|
|
167
|
+
"INSERT INTO agentmesh_kv (key, value, expires_at) "
|
|
168
|
+
"VALUES (:key, :value, NULL) "
|
|
169
|
+
"ON CONFLICT (key) DO UPDATE SET value = :value, expires_at = NULL",
|
|
170
|
+
{"key": key, "value": value},
|
|
171
|
+
)
|
|
172
|
+
await session.commit()
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
async def delete(self, key: str) -> bool:
|
|
176
|
+
"""Delete key."""
|
|
177
|
+
async with self._session_factory() as session:
|
|
178
|
+
result = await session.execute(
|
|
179
|
+
"DELETE FROM agentmesh_kv WHERE key = :key",
|
|
180
|
+
{"key": key},
|
|
181
|
+
)
|
|
182
|
+
await session.commit()
|
|
183
|
+
return result.rowcount > 0
|
|
184
|
+
|
|
185
|
+
async def exists(self, key: str) -> bool:
|
|
186
|
+
"""Check if key exists."""
|
|
187
|
+
result = await self.get(key)
|
|
188
|
+
return result is not None
|
|
189
|
+
|
|
190
|
+
# Hash Operations
|
|
191
|
+
|
|
192
|
+
async def hget(self, key: str, field: str) -> Optional[str]:
|
|
193
|
+
"""Get hash field value."""
|
|
194
|
+
async with self._session_factory() as session:
|
|
195
|
+
result = await session.execute(
|
|
196
|
+
"SELECT value FROM agentmesh_hash WHERE key = :key AND field = :field",
|
|
197
|
+
{"key": key, "field": field},
|
|
198
|
+
)
|
|
199
|
+
row = result.fetchone()
|
|
200
|
+
return row[0] if row else None
|
|
201
|
+
|
|
202
|
+
async def hset(self, key: str, field: str, value: str) -> bool:
|
|
203
|
+
"""Set hash field value."""
|
|
204
|
+
async with self._session_factory() as session:
|
|
205
|
+
await session.execute(
|
|
206
|
+
"INSERT INTO agentmesh_hash (key, field, value) "
|
|
207
|
+
"VALUES (:key, :field, :value) "
|
|
208
|
+
"ON CONFLICT (key, field) DO UPDATE SET value = :value",
|
|
209
|
+
{"key": key, "field": field, "value": value},
|
|
210
|
+
)
|
|
211
|
+
await session.commit()
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
async def hgetall(self, key: str) -> dict[str, str]:
|
|
215
|
+
"""Get all hash fields."""
|
|
216
|
+
async with self._session_factory() as session:
|
|
217
|
+
result = await session.execute(
|
|
218
|
+
"SELECT field, value FROM agentmesh_hash WHERE key = :key",
|
|
219
|
+
{"key": key},
|
|
220
|
+
)
|
|
221
|
+
return {row[0]: row[1] for row in result.fetchall()}
|
|
222
|
+
|
|
223
|
+
async def hdel(self, key: str, field: str) -> bool:
|
|
224
|
+
"""Delete hash field."""
|
|
225
|
+
async with self._session_factory() as session:
|
|
226
|
+
result = await session.execute(
|
|
227
|
+
"DELETE FROM agentmesh_hash WHERE key = :key AND field = :field",
|
|
228
|
+
{"key": key, "field": field},
|
|
229
|
+
)
|
|
230
|
+
await session.commit()
|
|
231
|
+
return result.rowcount > 0
|
|
232
|
+
|
|
233
|
+
async def hkeys(self, key: str) -> list[str]:
|
|
234
|
+
"""Get all hash field names."""
|
|
235
|
+
async with self._session_factory() as session:
|
|
236
|
+
result = await session.execute(
|
|
237
|
+
"SELECT field FROM agentmesh_hash WHERE key = :key",
|
|
238
|
+
{"key": key},
|
|
239
|
+
)
|
|
240
|
+
return [row[0] for row in result.fetchall()]
|
|
241
|
+
|
|
242
|
+
# List Operations
|
|
243
|
+
|
|
244
|
+
async def lpush(self, key: str, value: str) -> int:
|
|
245
|
+
"""Push value to head of list."""
|
|
246
|
+
async with self._session_factory() as session:
|
|
247
|
+
# Shift all indices up
|
|
248
|
+
await session.execute(
|
|
249
|
+
"UPDATE agentmesh_list SET idx = idx + 1 WHERE key = :key",
|
|
250
|
+
{"key": key},
|
|
251
|
+
)
|
|
252
|
+
# Insert at position 0
|
|
253
|
+
await session.execute(
|
|
254
|
+
"INSERT INTO agentmesh_list (key, idx, value) VALUES (:key, 0, :value)",
|
|
255
|
+
{"key": key, "value": value},
|
|
256
|
+
)
|
|
257
|
+
await session.commit()
|
|
258
|
+
# Get new length
|
|
259
|
+
result = await session.execute(
|
|
260
|
+
"SELECT COUNT(*) FROM agentmesh_list WHERE key = :key",
|
|
261
|
+
{"key": key},
|
|
262
|
+
)
|
|
263
|
+
return result.scalar()
|
|
264
|
+
|
|
265
|
+
async def rpush(self, key: str, value: str) -> int:
|
|
266
|
+
"""Push value to tail of list."""
|
|
267
|
+
async with self._session_factory() as session:
|
|
268
|
+
# Get max index
|
|
269
|
+
result = await session.execute(
|
|
270
|
+
"SELECT COALESCE(MAX(idx), -1) FROM agentmesh_list WHERE key = :key",
|
|
271
|
+
{"key": key},
|
|
272
|
+
)
|
|
273
|
+
max_idx = result.scalar()
|
|
274
|
+
# Insert at end
|
|
275
|
+
await session.execute(
|
|
276
|
+
"INSERT INTO agentmesh_list (key, idx, value) VALUES (:key, :idx, :value)",
|
|
277
|
+
{"key": key, "idx": max_idx + 1, "value": value},
|
|
278
|
+
)
|
|
279
|
+
await session.commit()
|
|
280
|
+
return max_idx + 2
|
|
281
|
+
|
|
282
|
+
async def lrange(self, key: str, start: int, stop: int) -> list[str]:
|
|
283
|
+
"""Get list range [start, stop]."""
|
|
284
|
+
async with self._session_factory() as session:
|
|
285
|
+
if stop == -1:
|
|
286
|
+
result = await session.execute(
|
|
287
|
+
"SELECT value FROM agentmesh_list WHERE key = :key AND idx >= :start ORDER BY idx",
|
|
288
|
+
{"key": key, "start": start},
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
result = await session.execute(
|
|
292
|
+
"SELECT value FROM agentmesh_list WHERE key = :key AND idx >= :start AND idx <= :stop ORDER BY idx",
|
|
293
|
+
{"key": key, "start": start, "stop": stop},
|
|
294
|
+
)
|
|
295
|
+
return [row[0] for row in result.fetchall()]
|
|
296
|
+
|
|
297
|
+
async def llen(self, key: str) -> int:
|
|
298
|
+
"""Get list length."""
|
|
299
|
+
async with self._session_factory() as session:
|
|
300
|
+
result = await session.execute(
|
|
301
|
+
"SELECT COUNT(*) FROM agentmesh_list WHERE key = :key",
|
|
302
|
+
{"key": key},
|
|
303
|
+
)
|
|
304
|
+
return result.scalar()
|
|
305
|
+
|
|
306
|
+
# Sorted Set Operations
|
|
307
|
+
|
|
308
|
+
async def zadd(self, key: str, score: float, member: str) -> bool:
|
|
309
|
+
"""Add member to sorted set with score."""
|
|
310
|
+
async with self._session_factory() as session:
|
|
311
|
+
await session.execute(
|
|
312
|
+
"INSERT INTO agentmesh_zset (key, member, score) "
|
|
313
|
+
"VALUES (:key, :member, :score) "
|
|
314
|
+
"ON CONFLICT (key, member) DO UPDATE SET score = :score",
|
|
315
|
+
{"key": key, "member": member, "score": score},
|
|
316
|
+
)
|
|
317
|
+
await session.commit()
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
async def zscore(self, key: str, member: str) -> Optional[float]:
|
|
321
|
+
"""Get score of member in sorted set."""
|
|
322
|
+
async with self._session_factory() as session:
|
|
323
|
+
result = await session.execute(
|
|
324
|
+
"SELECT score FROM agentmesh_zset WHERE key = :key AND member = :member",
|
|
325
|
+
{"key": key, "member": member},
|
|
326
|
+
)
|
|
327
|
+
row = result.fetchone()
|
|
328
|
+
return row[0] if row else None
|
|
329
|
+
|
|
330
|
+
async def zrange(
|
|
331
|
+
self,
|
|
332
|
+
key: str,
|
|
333
|
+
start: int,
|
|
334
|
+
stop: int,
|
|
335
|
+
with_scores: bool = False,
|
|
336
|
+
) -> list[str] | list[tuple[str, float]]:
|
|
337
|
+
"""Get sorted set range."""
|
|
338
|
+
async with self._session_factory() as session:
|
|
339
|
+
if stop == -1:
|
|
340
|
+
result = await session.execute(
|
|
341
|
+
"SELECT member, score FROM agentmesh_zset WHERE key = :key "
|
|
342
|
+
"ORDER BY score OFFSET :start",
|
|
343
|
+
{"key": key, "start": start},
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
result = await session.execute(
|
|
347
|
+
"SELECT member, score FROM agentmesh_zset WHERE key = :key "
|
|
348
|
+
"ORDER BY score LIMIT :limit OFFSET :start",
|
|
349
|
+
{"key": key, "start": start, "limit": stop - start + 1},
|
|
350
|
+
)
|
|
351
|
+
rows = result.fetchall()
|
|
352
|
+
if with_scores:
|
|
353
|
+
return [(row[0], row[1]) for row in rows]
|
|
354
|
+
return [row[0] for row in rows]
|
|
355
|
+
|
|
356
|
+
async def zrangebyscore(
|
|
357
|
+
self,
|
|
358
|
+
key: str,
|
|
359
|
+
min_score: float,
|
|
360
|
+
max_score: float,
|
|
361
|
+
with_scores: bool = False,
|
|
362
|
+
) -> list[str] | list[tuple[str, float]]:
|
|
363
|
+
"""Get sorted set range by score."""
|
|
364
|
+
async with self._session_factory() as session:
|
|
365
|
+
result = await session.execute(
|
|
366
|
+
"SELECT member, score FROM agentmesh_zset "
|
|
367
|
+
"WHERE key = :key AND score >= :min AND score <= :max ORDER BY score",
|
|
368
|
+
{"key": key, "min": min_score, "max": max_score},
|
|
369
|
+
)
|
|
370
|
+
rows = result.fetchall()
|
|
371
|
+
if with_scores:
|
|
372
|
+
return [(row[0], row[1]) for row in rows]
|
|
373
|
+
return [row[0] for row in rows]
|
|
374
|
+
|
|
375
|
+
# Atomic Operations
|
|
376
|
+
|
|
377
|
+
async def incr(self, key: str) -> int:
|
|
378
|
+
"""Increment value atomically."""
|
|
379
|
+
return await self.incrby(key, 1)
|
|
380
|
+
|
|
381
|
+
async def decr(self, key: str) -> int:
|
|
382
|
+
"""Decrement value atomically."""
|
|
383
|
+
return await self.incrby(key, -1)
|
|
384
|
+
|
|
385
|
+
async def incrby(self, key: str, amount: int) -> int:
|
|
386
|
+
"""Increment value by amount."""
|
|
387
|
+
async with self._session_factory() as session:
|
|
388
|
+
# Use PostgreSQL's atomic UPDATE ... RETURNING
|
|
389
|
+
result = await session.execute(
|
|
390
|
+
"INSERT INTO agentmesh_kv (key, value) VALUES (:key, :amount) "
|
|
391
|
+
"ON CONFLICT (key) DO UPDATE SET value = "
|
|
392
|
+
"(CAST(agentmesh_kv.value AS INTEGER) + :amount)::TEXT "
|
|
393
|
+
"RETURNING CAST(value AS INTEGER)",
|
|
394
|
+
{"key": key, "amount": str(amount)},
|
|
395
|
+
)
|
|
396
|
+
await session.commit()
|
|
397
|
+
return result.scalar()
|
|
398
|
+
|
|
399
|
+
# Batch Operations
|
|
400
|
+
|
|
401
|
+
async def mget(self, keys: list[str]) -> list[Optional[str]]:
|
|
402
|
+
"""Get multiple values."""
|
|
403
|
+
async with self._session_factory() as session:
|
|
404
|
+
result = await session.execute(
|
|
405
|
+
"SELECT key, value FROM agentmesh_kv WHERE key = ANY(:keys) "
|
|
406
|
+
"AND (expires_at IS NULL OR expires_at > NOW())",
|
|
407
|
+
{"keys": keys},
|
|
408
|
+
)
|
|
409
|
+
values_dict = {row[0]: row[1] for row in result.fetchall()}
|
|
410
|
+
return [values_dict.get(key) for key in keys]
|
|
411
|
+
|
|
412
|
+
async def mset(self, mapping: dict[str, str]) -> bool:
|
|
413
|
+
"""Set multiple key-value pairs."""
|
|
414
|
+
async with self._session_factory() as session:
|
|
415
|
+
for key, value in mapping.items():
|
|
416
|
+
await session.execute(
|
|
417
|
+
"INSERT INTO agentmesh_kv (key, value) VALUES (:key, :value) "
|
|
418
|
+
"ON CONFLICT (key) DO UPDATE SET value = :value",
|
|
419
|
+
{"key": key, "value": value},
|
|
420
|
+
)
|
|
421
|
+
await session.commit()
|
|
422
|
+
return True
|
|
423
|
+
|
|
424
|
+
# Pattern Operations
|
|
425
|
+
|
|
426
|
+
async def keys(self, pattern: str) -> list[str]:
|
|
427
|
+
"""Get keys matching pattern."""
|
|
428
|
+
# Convert glob pattern to SQL LIKE pattern
|
|
429
|
+
sql_pattern = pattern.replace("*", "%").replace("?", "_")
|
|
430
|
+
async with self._session_factory() as session:
|
|
431
|
+
result = await session.execute(
|
|
432
|
+
"SELECT key FROM agentmesh_kv WHERE key LIKE :pattern "
|
|
433
|
+
"AND (expires_at IS NULL OR expires_at > NOW())",
|
|
434
|
+
{"pattern": sql_pattern},
|
|
435
|
+
)
|
|
436
|
+
return [row[0] for row in result.fetchall()]
|
|
437
|
+
|
|
438
|
+
async def scan(
|
|
439
|
+
self,
|
|
440
|
+
cursor: int = 0,
|
|
441
|
+
match: Optional[str] = None,
|
|
442
|
+
count: int = 100,
|
|
443
|
+
) -> tuple[int, list[str]]:
|
|
444
|
+
"""Scan keys with cursor."""
|
|
445
|
+
async with self._session_factory() as session:
|
|
446
|
+
if match:
|
|
447
|
+
sql_pattern = match.replace("*", "%").replace("?", "_")
|
|
448
|
+
result = await session.execute(
|
|
449
|
+
"SELECT key FROM agentmesh_kv WHERE key LIKE :pattern "
|
|
450
|
+
"AND (expires_at IS NULL OR expires_at > NOW()) "
|
|
451
|
+
"ORDER BY key LIMIT :count OFFSET :cursor",
|
|
452
|
+
{"pattern": sql_pattern, "count": count, "cursor": cursor},
|
|
453
|
+
)
|
|
454
|
+
else:
|
|
455
|
+
result = await session.execute(
|
|
456
|
+
"SELECT key FROM agentmesh_kv "
|
|
457
|
+
"WHERE expires_at IS NULL OR expires_at > NOW() "
|
|
458
|
+
"ORDER BY key LIMIT :count OFFSET :cursor",
|
|
459
|
+
{"count": count, "cursor": cursor},
|
|
460
|
+
)
|
|
461
|
+
keys = [row[0] for row in result.fetchall()]
|
|
462
|
+
new_cursor = cursor + count if len(keys) == count else 0
|
|
463
|
+
return new_cursor, keys
|