File size: 8,305 Bytes
66f749a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
"""

Agent memory system using ChromaDB for vector storage.



If ChromaDB (HTTP server) is not reachable, all operations become no-ops

so the simulation continues without memory — never falls back to the

embedded in-memory client which crashes on Windows (hnswlib access violation).

"""
import logging
import os
import time
from typing import List, Dict, Any, Optional

# Suppress PostHog telemetry noise (capture() argument mismatch)
os.environ.setdefault("ANONYMIZED_TELEMETRY", "False")
import chromadb

logger = logging.getLogger(__name__)


class AgentMemoryStore:
    """

    Vector database for storing and retrieving agent memories.



    Uses ChromaDB (HTTP mode only) for:

    - Agent profile embeddings

    - Past experiences and interactions

    - RAG (Retrieval Augmented Generation) context for decisions



    If ChromaDB is not available the store silently becomes a no-op so the

    simulation can run without memory rather than crashing.

    """

    def __init__(

        self,

        chroma_host: str = "localhost",

        chroma_port: int = 8000,

        collection_name: str = "agent_memories",

        ssl: bool = False,

    ):
        """

        Initialize connection to ChromaDB HTTP server.



        Falls back to disabled (no-op) mode instead of in-memory mode to

        avoid the Windows hnswlib C++ access violation crash.

        """
        self.collection = None  # None means disabled / no-op

        try:
            from chromadb.config import Settings as ChromaSettings
            client = chromadb.HttpClient(
                host=chroma_host,
                port=chroma_port,
                ssl=ssl,
                settings=ChromaSettings(
                    chroma_client_auth_provider=None,
                    anonymized_telemetry=False,  # suppress telemetry noise
                ),
            )

            # Heartbeat check — raises immediately if server is down
            client.heartbeat()

            self.collection = client.get_or_create_collection(
                name=collection_name,
            )
            logger.info(f"Connected to ChromaDB at {chroma_host}:{chroma_port}")

        except Exception as e:
            # Do NOT fall back to chromadb.Client() — the embedded hnswlib
            # backend crashes on Windows with an access violation.
            logger.info(
                f"ChromaDB not available at {chroma_host}:{chroma_port} "
                f"({type(e).__name__}: {e}). "
                f"Running without agent memory (simulation unaffected)."
            )
            self.collection = None

    @property
    def available(self) -> bool:
        """True if ChromaDB is connected and usable."""
        return self.collection is not None

    def create_agent_profile(self, agent_id: str, profile: Dict[str, Any]):
        """

        Store agent's demographic profile and values.

        No-op if ChromaDB is unavailable.

        """
        if not self.available:
            return

        values = profile.get('values', [])
        values_str = ', '.join(values) if values else 'Not specified'

        profile_text = f"""

Demographics:

- Age: {profile.get('age', 'Unknown')}

- Gender: {profile.get('gender', 'Unknown')}

- Location: {profile.get('location', 'Unknown')}

- Education: {profile.get('education', 'Not specified')}

- Occupation: {profile.get('occupation', 'Not specified')}



Core values: {values_str}

"""

        # Sanitize metadata: ChromaDB only accepts str, int, float, bool
        sanitized_profile = {}
        for key, value in profile.items():
            if isinstance(value, (str, int, float, bool)):
                sanitized_profile[key] = value
            elif isinstance(value, list):
                sanitized_profile[key] = ', '.join(str(v) for v in value)
            elif value is None:
                sanitized_profile[key] = ''
            else:
                sanitized_profile[key] = str(value)

        metadata = {"agent_id": agent_id, "type": "profile", **sanitized_profile}

        try:
            existing = self.collection.get(ids=[f"{agent_id}_profile"])
            if existing['ids']:
                self.collection.update(
                    ids=[f"{agent_id}_profile"],
                    documents=[profile_text],
                    metadatas=[metadata]
                )
            else:
                self.collection.add(
                    ids=[f"{agent_id}_profile"],
                    documents=[profile_text],
                    metadatas=[metadata]
                )
            logger.debug(f"Stored profile for agent {agent_id}")

        except Exception as e:
            logger.error(f"Failed to store profile for {agent_id}: {e}")

    def add_experience(

        self,

        agent_id: str,

        experience: str,

        experience_type: str = "interaction"

    ):
        """

        Store a past interaction or experience.

        No-op if ChromaDB is unavailable.

        """
        if not self.available:
            return

        experience_id = f"{agent_id}_exp_{int(time.time() * 1000)}"

        try:
            self.collection.add(
                ids=[experience_id],
                documents=[experience],
                metadatas=[{
                    "agent_id": agent_id,
                    "type": experience_type,
                    "timestamp": time.time()
                }]
            )
            logger.debug(f"Added experience for {agent_id}: {experience[:50]}...")

        except Exception as e:
            logger.error(f"Failed to add experience for {agent_id}: {e}")

    def query_relevant_context(

        self,

        agent_id: str,

        query: str,

        n_results: int = 3

    ) -> List[str]:
        """

        Retrieve relevant memories for decision-making (RAG).

        Returns empty list if ChromaDB is unavailable.

        """
        if not self.available:
            return []

        try:
            results = self.collection.query(
                query_texts=[query],
                where={"agent_id": agent_id},
                n_results=n_results
            )

            if results and results['documents']:
                return results['documents'][0]
            return []

        except Exception as e:
            logger.error(f"Failed to query memories for {agent_id}: {e}")
            return []

    def get_agent_profile(self, agent_id: str) -> Optional[Dict[str, Any]]:
        """

        Get stored profile for an agent.

        Returns None if ChromaDB is unavailable.

        """
        if not self.available:
            return None

        try:
            results = self.collection.get(
                ids=[f"{agent_id}_profile"],
                include=["metadatas"]
            )

            if results and results['metadatas']:
                return results['metadatas'][0]
            return None

        except Exception as e:
            logger.error(f"Failed to get profile for {agent_id}: {e}")
            return None

    def clear_agent_memories(self, agent_id: str):
        """

        Remove all memories for an agent.

        No-op if ChromaDB is unavailable.

        """
        if not self.available:
            return

        try:
            results = self.collection.get(
                where={"agent_id": agent_id},
                include=["metadatas"]
            )

            if results and results['ids']:
                self.collection.delete(ids=results['ids'])
                logger.info(f"Cleared {len(results['ids'])} memories for {agent_id}")

        except Exception as e:
            logger.error(f"Failed to clear memories for {agent_id}: {e}")

    def clear_all(self):
        """

        Clear entire collection.

        No-op if ChromaDB is unavailable.

        """
        if not self.available:
            return

        try:
            self.collection.delete(where={})
            logger.info("Cleared all agent memories")

        except Exception as e:
            logger.error(f"Failed to clear all memories: {e}")