agentic-ai-kit 0.1.1__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.
Files changed (45) hide show
  1. agentic_ai/__init__.py +24 -0
  2. agentic_ai/agents/__init__.py +17 -0
  3. agentic_ai/agents/analyst_agent.py +239 -0
  4. agentic_ai/agents/auto_model_agent.py +156 -0
  5. agentic_ai/agents/base.py +280 -0
  6. agentic_ai/agents/json_agent.py +73 -0
  7. agentic_ai/agents/mle_agent.py +590 -0
  8. agentic_ai/agents/reasoning_agent.py +97 -0
  9. agentic_ai/agents/tool_agent.py +171 -0
  10. agentic_ai/examples/01_talking_agents.py +9 -0
  11. agentic_ai/examples/02_personality_and_memory.py +12 -0
  12. agentic_ai/examples/03_tool_agent_weather.py +10 -0
  13. agentic_ai/examples/04_json_agent.py +11 -0
  14. agentic_ai/examples/05_reasoning_agent.py +9 -0
  15. agentic_ai/examples/06_supervisor.py +16 -0
  16. agentic_ai/examples/07_parallel_agents.py +15 -0
  17. agentic_ai/examples/08_debate.py +16 -0
  18. agentic_ai/examples/09_long_term_memory.py +23 -0
  19. agentic_ai/examples/10_rag_agent.py +29 -0
  20. agentic_ai/examples/11_mcp_demo.py +41 -0
  21. agentic_ai/examples/__init__.py +1 -0
  22. agentic_ai/mcp/__init__.py +4 -0
  23. agentic_ai/mcp/client.py +55 -0
  24. agentic_ai/mcp/server.py +133 -0
  25. agentic_ai/memory/__init__.py +5 -0
  26. agentic_ai/memory/long_term.py +104 -0
  27. agentic_ai/memory/shared.py +52 -0
  28. agentic_ai/memory/short_term.py +43 -0
  29. agentic_ai/patterns/__init__.py +5 -0
  30. agentic_ai/patterns/debate.py +49 -0
  31. agentic_ai/patterns/orchestrator.py +91 -0
  32. agentic_ai/patterns/parallel.py +39 -0
  33. agentic_ai/py.typed +0 -0
  34. agentic_ai/rag/__init__.py +6 -0
  35. agentic_ai/rag/chunker.py +23 -0
  36. agentic_ai/rag/embedder.py +30 -0
  37. agentic_ai/rag/rag_agent.py +42 -0
  38. agentic_ai/rag/vector_store.py +51 -0
  39. agentic_ai/tools/__init__.py +8 -0
  40. agentic_ai/tools/builtins.py +51 -0
  41. agentic_ai/tools/registry.py +120 -0
  42. agentic_ai_kit-0.1.1.dist-info/METADATA +201 -0
  43. agentic_ai_kit-0.1.1.dist-info/RECORD +45 -0
  44. agentic_ai_kit-0.1.1.dist-info/WHEEL +5 -0
  45. agentic_ai_kit-0.1.1.dist-info/top_level.txt +1 -0
agentic_ai/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ agentic_ai — A progressive framework for building agentic AI systems with Google Gemini.
3
+ """
4
+
5
+ from agentic_ai.agents.base import BaseAgent
6
+ from agentic_ai.agents.tool_agent import ToolAgent
7
+ from agentic_ai.agents.json_agent import JsonAgent
8
+ from agentic_ai.agents.reasoning_agent import ReasoningAgent
9
+ from agentic_ai.memory.short_term import ShortTermMemory
10
+ from agentic_ai.memory.long_term import LongTermMemory
11
+ from agentic_ai.memory.shared import SharedMemory
12
+ from agentic_ai.agents.auto_model_agent import AutoModelAgent
13
+
14
+ __version__ = "0.1.0"
15
+ __all__ = [
16
+ "BaseAgent",
17
+ "ToolAgent",
18
+ "JsonAgent",
19
+ "ReasoningAgent",
20
+ "ShortTermMemory",
21
+ "LongTermMemory",
22
+ "SharedMemory",
23
+ "AutoModelAgent",
24
+ ]
@@ -0,0 +1,17 @@
1
+ from agentic_ai.agents.base import BaseAgent
2
+ from agentic_ai.agents.tool_agent import ToolAgent
3
+ from agentic_ai.agents.json_agent import JsonAgent
4
+ from agentic_ai.agents.reasoning_agent import ReasoningAgent
5
+ from agentic_ai.agents.analyst_agent import AnalystAgent
6
+ from agentic_ai.agents.mle_agent import MLEAgent
7
+ from agentic_ai.agents.auto_model_agent import AutoModelAgent
8
+
9
+ __all__ = [
10
+ "BaseAgent",
11
+ "ToolAgent",
12
+ "JsonAgent",
13
+ "ReasoningAgent",
14
+ "AnalystAgent",
15
+ "MLEAgent",
16
+ "AutoModelAgent",
17
+ ]
@@ -0,0 +1,239 @@
1
+ """
2
+ AnalystAgent: dataframe-aware analytics specialist agent.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ import pandas as pd
10
+
11
+ from agentic_ai.agents.base import BaseAgent
12
+
13
+
14
+ _ANALYST_PROMPT = """
15
+ You are an elite Decision Scientist.
16
+
17
+ You help users analyze pandas DataFrames and business datasets.
18
+
19
+ You are expected to reason using:
20
+ - dataframe schema
21
+ - row counts
22
+ - missing values
23
+ - numeric summaries
24
+ - categorical distributions
25
+ - group-level metrics
26
+ - data quality risks
27
+ - business interpretation
28
+
29
+ Always separate:
30
+ 1. what the data says
31
+ 2. what it might mean
32
+ 3. what should be checked next
33
+
34
+ Preferred response structure:
35
+
36
+ BUSINESS QUESTION
37
+ DATA READ
38
+ KEY OBSERVATIONS
39
+ DATA QUALITY RISKS
40
+ RECOMMENDED ANALYSIS
41
+ DECISION SUMMARY
42
+
43
+ Do not invent numbers.
44
+ If dataframe context is provided, use it explicitly.
45
+ """.strip()
46
+
47
+
48
+ class AnalystAgent(BaseAgent):
49
+ """Analytics-focused specialist agent with dataframe tools."""
50
+
51
+ def __init__(
52
+ self,
53
+ name: str = "Analyst",
54
+ domain_context: str | None = None,
55
+ model: str = "gemini-2.5-flash-lite",
56
+ api_key: str | None = None,
57
+ memory_window: int = 3,
58
+ max_turns: int | None = None,
59
+ thinking_budget: int = 0,
60
+ ):
61
+ prompt = _ANALYST_PROMPT
62
+
63
+ if domain_context:
64
+ prompt += f"\n\nDOMAIN CONTEXT:\n{domain_context.strip()}"
65
+
66
+ super().__init__(
67
+ name=name,
68
+ sys_prompt=prompt,
69
+ model=model,
70
+ api_key=api_key,
71
+ memory_window=memory_window,
72
+ max_turns=max_turns,
73
+ thinking_budget=thinking_budget,
74
+ )
75
+
76
+ # ------------------------------------------------------------------
77
+ # DataFrame tools
78
+ # ------------------------------------------------------------------
79
+
80
+ def profile_dataframe(self, df: pd.DataFrame) -> dict[str, Any]:
81
+ """Return a consistent dataframe profile."""
82
+
83
+ self._validate_dataframe(df)
84
+
85
+ return {
86
+ "rows": int(df.shape[0]),
87
+ "columns": int(df.shape[1]),
88
+ "column_names": list(df.columns),
89
+ "dtypes": {col: str(dtype) for col, dtype in df.dtypes.items()},
90
+ "missing_values": df.isna().sum().to_dict(),
91
+ "missing_percentage": (
92
+ df.isna().mean().mul(100).round(2).to_dict()
93
+ ),
94
+ "duplicate_rows": int(df.duplicated().sum()),
95
+ "numeric_columns": list(df.select_dtypes(include="number").columns),
96
+ "categorical_columns": list(
97
+ df.select_dtypes(include=["object", "category", "bool"]).columns
98
+ ),
99
+ "datetime_columns": list(
100
+ df.select_dtypes(include=["datetime", "datetimetz"]).columns
101
+ ),
102
+ }
103
+
104
+ def numeric_summary(self, df: pd.DataFrame) -> dict[str, Any]:
105
+ """Return numeric column summary."""
106
+
107
+ self._validate_dataframe(df)
108
+
109
+ numeric_df = df.select_dtypes(include="number")
110
+
111
+ if numeric_df.empty:
112
+ return {"message": "No numeric columns found."}
113
+
114
+ return numeric_df.describe().round(4).to_dict()
115
+
116
+ def categorical_summary(
117
+ self,
118
+ df: pd.DataFrame,
119
+ top_n: int = 10,
120
+ ) -> dict[str, Any]:
121
+ """Return top category counts for categorical columns."""
122
+
123
+ self._validate_dataframe(df)
124
+
125
+ categorical_df = df.select_dtypes(include=["object", "category", "bool"])
126
+
127
+ if categorical_df.empty:
128
+ return {"message": "No categorical columns found."}
129
+
130
+ summary = {}
131
+
132
+ for col in categorical_df.columns:
133
+ summary[col] = (
134
+ categorical_df[col]
135
+ .value_counts(dropna=False)
136
+ .head(top_n)
137
+ .to_dict()
138
+ )
139
+
140
+ return summary
141
+
142
+ def groupby_summary(
143
+ self,
144
+ df: pd.DataFrame,
145
+ group_col: str,
146
+ metric_col: str,
147
+ agg: str = "mean",
148
+ ) -> dict[str, Any]:
149
+ """Return grouped metric summary."""
150
+
151
+ self._validate_dataframe(df)
152
+ self._validate_columns(df, [group_col, metric_col])
153
+
154
+ allowed_aggs = {"mean", "sum", "count", "median", "min", "max"}
155
+
156
+ if agg not in allowed_aggs:
157
+ raise ValueError(f"agg must be one of {sorted(allowed_aggs)}")
158
+
159
+ result = (
160
+ df.groupby(group_col, dropna=False)[metric_col]
161
+ .agg(agg)
162
+ .reset_index()
163
+ .sort_values(metric_col, ascending=False)
164
+ )
165
+
166
+ return result.to_dict(orient="records")
167
+
168
+ def correlation_summary(self, df: pd.DataFrame) -> dict[str, Any]:
169
+ """Return numeric correlation matrix."""
170
+
171
+ self._validate_dataframe(df)
172
+
173
+ numeric_df = df.select_dtypes(include="number")
174
+
175
+ if numeric_df.shape[1] < 2:
176
+ return {"message": "At least two numeric columns are needed."}
177
+
178
+ return numeric_df.corr().round(4).to_dict()
179
+
180
+ def dataframe_context(
181
+ self,
182
+ df: pd.DataFrame,
183
+ include_numeric: bool = True,
184
+ include_categorical: bool = True,
185
+ ) -> str:
186
+ """Create dataframe context for the LLM."""
187
+
188
+ context = {
189
+ "profile": self.profile_dataframe(df),
190
+ }
191
+
192
+ if include_numeric:
193
+ context["numeric_summary"] = self.numeric_summary(df)
194
+
195
+ if include_categorical:
196
+ context["categorical_summary"] = self.categorical_summary(df)
197
+
198
+ return str(context)
199
+
200
+ def analyze_dataframe(
201
+ self,
202
+ df: pd.DataFrame,
203
+ question: str,
204
+ stream: bool = True,
205
+ ) -> str:
206
+ """Analyze a dataframe using generated dataframe context."""
207
+
208
+ context = self.dataframe_context(df)
209
+
210
+ prompt = f"""
211
+ Analyze the dataframe for the following question.
212
+
213
+ QUESTION:
214
+ {question}
215
+
216
+ DATAFRAME CONTEXT:
217
+ {context}
218
+ """.strip()
219
+
220
+ return self.think(prompt, stream=stream)
221
+
222
+ # ------------------------------------------------------------------
223
+ # Validation helpers
224
+ # ------------------------------------------------------------------
225
+
226
+ @staticmethod
227
+ def _validate_dataframe(df: pd.DataFrame) -> None:
228
+ if not isinstance(df, pd.DataFrame):
229
+ raise TypeError("Expected a pandas DataFrame.")
230
+
231
+ if df.empty:
232
+ raise ValueError("DataFrame is empty.")
233
+
234
+ @staticmethod
235
+ def _validate_columns(df: pd.DataFrame, columns: list[str]) -> None:
236
+ missing = [col for col in columns if col not in df.columns]
237
+
238
+ if missing:
239
+ raise ValueError(f"Missing columns: {missing}")
@@ -0,0 +1,156 @@
1
+ """
2
+ AutoModelAgent: end-to-end dataframe-to-model agent.
3
+
4
+ The user gives:
5
+ - dataframe
6
+ - target column
7
+ - business objective
8
+
9
+ The agent:
10
+ - profiles the data
11
+ - designs modeling policy
12
+ - trains models using MLEAgent
13
+ - returns results and interpretation
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ import pandas as pd
21
+
22
+ from agentic_ai.agents.mle_agent import MLEAgent
23
+
24
+
25
+ class AutoModelAgent(MLEAgent):
26
+ """End-to-end modeling agent built on top of MLEAgent."""
27
+
28
+ def run(
29
+ self,
30
+ df: pd.DataFrame,
31
+ target_col: str,
32
+ objective: str,
33
+ drop_columns: list[str] | None = None,
34
+ test_size: float = 0.2,
35
+ random_state: int = 42,
36
+ stream: bool = True,
37
+ interpret: bool = True,
38
+ ) -> dict[str, Any]:
39
+ """Look at data, design policy, train model, and explain results."""
40
+
41
+ self._validate_dataframe(df)
42
+ self._validate_columns(df, [target_col])
43
+
44
+ profile = self.profile_dataframe(df)
45
+ target = self.target_summary(df, target_col)
46
+ features = self.feature_summary(df, target_col)
47
+ leakage = self.leakage_scan(df, target_col)
48
+
49
+
50
+ if not interpret:
51
+ model_result = self.train_model(
52
+ df=df,
53
+ target_col=target_col,
54
+ test_size=test_size,
55
+ random_state=random_state,
56
+ drop_columns=drop_columns,
57
+ )
58
+
59
+ return {
60
+ "objective": objective,
61
+ "target_column": target_col,
62
+ "data_profile": profile,
63
+ "target_summary": target,
64
+ "feature_summary": features,
65
+ "leakage_scan": leakage,
66
+ "modeling_policy": None,
67
+ "interpretation": None,
68
+ "model_result": model_result,
69
+ "best_model": model_result["best_model"],
70
+ "best_score": model_result["best_score"],
71
+ "best_pipeline": model_result["best_pipeline"],
72
+ }
73
+
74
+ policy_prompt = f"""
75
+ You are about to create a machine learning model.
76
+
77
+ BUSINESS OBJECTIVE:
78
+ {objective}
79
+
80
+ TARGET COLUMN:
81
+ {target_col}
82
+
83
+ DATA PROFILE:
84
+ {profile}
85
+
86
+ TARGET SUMMARY:
87
+ {target}
88
+
89
+ FEATURE SUMMARY:
90
+ {features}
91
+
92
+ LEAKAGE SCAN:
93
+ {leakage}
94
+
95
+ Design a concise modeling policy before training.
96
+
97
+ Include:
98
+ 1. problem type
99
+ 2. usable features
100
+ 3. columns to drop, if any
101
+ 4. leakage risks
102
+ 5. train/test approach
103
+ 6. model candidates
104
+ 7. evaluation metric
105
+ 8. expected limitations
106
+
107
+ Return a practical modeling policy.
108
+ """.strip()
109
+
110
+ modeling_policy = self.think(policy_prompt, stream=stream)
111
+
112
+ model_result = self.train_model(
113
+ df=df,
114
+ target_col=target_col,
115
+ test_size=test_size,
116
+ random_state=random_state,
117
+ drop_columns=drop_columns,
118
+ )
119
+
120
+ result_prompt = f"""
121
+ A model has now been trained.
122
+
123
+ BUSINESS OBJECTIVE:
124
+ {objective}
125
+
126
+ MODELING POLICY:
127
+ {modeling_policy}
128
+
129
+ MODEL RESULT:
130
+ {self._safe_model_result_for_llm(model_result)}
131
+
132
+ Explain:
133
+ 1. what model was created
134
+ 2. which model performed best
135
+ 3. what the metrics mean
136
+ 4. whether the result is usable
137
+ 5. what should be improved next
138
+ 6. how this could be deployed
139
+ """.strip()
140
+
141
+ interpretation = self.think(result_prompt, stream=stream)
142
+
143
+ return {
144
+ "objective": objective,
145
+ "target_column": target_col,
146
+ "data_profile": profile,
147
+ "target_summary": target,
148
+ "feature_summary": features,
149
+ "leakage_scan": leakage,
150
+ "modeling_policy": modeling_policy,
151
+ "model_result": model_result,
152
+ "interpretation": interpretation,
153
+ "best_model": model_result["best_model"],
154
+ "best_score": model_result["best_score"],
155
+ "best_pipeline": model_result["best_pipeline"],
156
+ }
@@ -0,0 +1,280 @@
1
+ """
2
+ BaseAgent: the foundation for all agents in agentic_ai.
3
+
4
+ Every agent has:
5
+ - A name and system prompt that define its persona
6
+ - Short-term memory for recent context
7
+ - Optional max-turn retention at the interface level
8
+ - LLM-based fact extraction for lightweight user memory
9
+ - A `think(input_text)` method that streams a Gemini response
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+
17
+ from google import genai
18
+ from google.genai import types
19
+
20
+
21
+ class BaseAgent:
22
+ """Core agent backed by a Gemini model."""
23
+
24
+ def __init__(
25
+ self,
26
+ name: str,
27
+ sys_prompt: str,
28
+ model: str = "gemini-2.5-flash-lite",
29
+ api_key: str | None = None,
30
+ memory_window: int = 3,
31
+ max_turns: int | None = None,
32
+ max_facts: int = 50,
33
+ extract_memory: bool = True,
34
+ thinking_budget: int = 0,
35
+ ):
36
+ self.name = name
37
+ self.sys_prompt = sys_prompt
38
+ self.model = model
39
+ self.memory_window = memory_window
40
+ self.max_turns = max_turns
41
+ self.max_facts = max_facts
42
+ self.extract_memory = extract_memory
43
+ self.thinking_budget = thinking_budget
44
+
45
+ self.memory: list[tuple[str, str]] = []
46
+ self.facts_store: list[dict[str, object]] = []
47
+
48
+ key = api_key or os.environ.get("GEMINI_API_KEY")
49
+ if not key:
50
+ raise ValueError(
51
+ "No Gemini API key found. Pass api_key= or set GEMINI_API_KEY."
52
+ )
53
+
54
+ self.client = genai.Client(api_key=key)
55
+
56
+ # ------------------------------------------------------------------
57
+ # Memory helpers
58
+ # ------------------------------------------------------------------
59
+
60
+ def extract_facts(self, text: str) -> None:
61
+ """Extract durable user facts from input using Gemini."""
62
+
63
+ if not self.extract_memory:
64
+ return
65
+
66
+ extraction_prompt = f"""
67
+ You are a memory extraction system.
68
+
69
+ Extract durable facts from the user's message.
70
+
71
+ A durable fact is something likely to remain useful in future conversations.
72
+
73
+ Examples of durable facts:
74
+ - User works as a data scientist.
75
+ - User prefers Python.
76
+ - User is building an agentic AI framework.
77
+ - User is learning PyTorch.
78
+ - User works on retail analytics.
79
+
80
+ Do not extract:
81
+ - Temporary requests
82
+ - Greetings
83
+ - One-off questions
84
+ - Generic statements
85
+ - Facts about the assistant
86
+ - Very sensitive personal information
87
+
88
+ Return ONLY valid JSON in this exact format:
89
+
90
+ [
91
+ {{
92
+ "fact": "User is building an agentic AI framework.",
93
+ "confidence": 0.95
94
+ }}
95
+ ]
96
+
97
+ If there are no durable facts, return:
98
+
99
+ []
100
+
101
+ User message:
102
+ {text}
103
+ """.strip()
104
+
105
+ try:
106
+ response = self.client.models.generate_content(
107
+ model=self.model,
108
+ contents=extraction_prompt,
109
+ config=types.GenerateContentConfig(
110
+ temperature=0,
111
+ thinking_config=types.ThinkingConfig(thinking_budget=0),
112
+ ),
113
+ )
114
+
115
+ raw_output = (response.text or "").strip()
116
+ extracted = json.loads(raw_output)
117
+
118
+ if not isinstance(extracted, list):
119
+ return
120
+
121
+ for item in extracted:
122
+ if not isinstance(item, dict):
123
+ continue
124
+
125
+ fact = str(item.get("fact", "")).strip()
126
+ confidence = item.get("confidence", 0.0)
127
+
128
+ try:
129
+ confidence = float(confidence)
130
+ except (TypeError, ValueError):
131
+ confidence = 0.0
132
+
133
+ if not fact:
134
+ continue
135
+
136
+ if confidence < 0.6:
137
+ continue
138
+
139
+ if self._fact_exists(fact):
140
+ continue
141
+
142
+ self.facts_store.append(
143
+ {
144
+ "fact": fact,
145
+ "confidence": confidence,
146
+ "source": text,
147
+ }
148
+ )
149
+
150
+ self.trim_facts()
151
+
152
+ except Exception:
153
+ # Memory extraction should never break the main agent response.
154
+ return
155
+
156
+ def _fact_exists(self, new_fact: str) -> bool:
157
+ """Check whether a fact already exists in memory."""
158
+
159
+ normalized_new = new_fact.lower().strip().rstrip(".")
160
+
161
+ for item in self.facts_store:
162
+ existing = str(item.get("fact", "")).lower().strip().rstrip(".")
163
+ if existing == normalized_new:
164
+ return True
165
+
166
+ return False
167
+
168
+ def build_context(self, input_text: str) -> str:
169
+ """Assemble facts, recent history, and the new message."""
170
+
171
+ recent_turns = self.memory[-self.memory_window :]
172
+
173
+ history = "\n".join(
174
+ f"user: {user_input}\nassistant: {agent_output}"
175
+ for user_input, agent_output in recent_turns
176
+ )
177
+
178
+ if self.facts_store:
179
+ facts_text = "\n".join(
180
+ f"- {item['fact']}" for item in self.facts_store
181
+ )
182
+ else:
183
+ facts_text = "(none)"
184
+
185
+ return (
186
+ f"FACTS:\n{facts_text}\n\n"
187
+ f"HISTORY:\n{history}\n\n"
188
+ f"NEW MESSAGE:\n{input_text}"
189
+ )
190
+
191
+ def trim_memory(self) -> None:
192
+ """Limit retained conversation turns at the interface level."""
193
+
194
+ if self.max_turns is not None:
195
+ if self.max_turns < 0:
196
+ raise ValueError("max_turns must be None or a non-negative integer.")
197
+
198
+ self.memory = self.memory[-self.max_turns :]
199
+
200
+ def trim_facts(self) -> None:
201
+ """Limit number of stored facts."""
202
+
203
+ if self.max_facts < 0:
204
+ raise ValueError("max_facts must be a non-negative integer.")
205
+
206
+ self.facts_store = self.facts_store[-self.max_facts :]
207
+
208
+ def clear_memory(self) -> None:
209
+ """Wipe short-term memory and facts store."""
210
+
211
+ self.memory.clear()
212
+ self.facts_store.clear()
213
+
214
+ # ------------------------------------------------------------------
215
+ # Core generation
216
+ # ------------------------------------------------------------------
217
+
218
+ def think(
219
+ self,
220
+ input_text: str,
221
+ use_memory: bool = True,
222
+ stream: bool = True,
223
+ ) -> str:
224
+ """Send input to Gemini and return the full response string."""
225
+
226
+ self.extract_facts(input_text)
227
+ prompt = self.build_context(input_text) if use_memory else input_text
228
+
229
+ contents = [
230
+ types.Content(
231
+ role="user",
232
+ parts=[types.Part.from_text(text=prompt)],
233
+ )
234
+ ]
235
+
236
+ config = types.GenerateContentConfig(
237
+ thinking_config=types.ThinkingConfig(
238
+ thinking_budget=self.thinking_budget
239
+ ),
240
+ system_instruction=[
241
+ types.Part.from_text(text=self.sys_prompt)
242
+ ],
243
+ )
244
+
245
+ response_text = ""
246
+
247
+ if stream:
248
+ for chunk in self.client.models.generate_content_stream(
249
+ model=self.model,
250
+ contents=contents,
251
+ config=config,
252
+ ):
253
+ if chunk.text:
254
+ print(chunk.text, end="", flush=True)
255
+ response_text += chunk.text
256
+
257
+ print()
258
+
259
+ else:
260
+ response = self.client.models.generate_content(
261
+ model=self.model,
262
+ contents=contents,
263
+ config=config,
264
+ )
265
+ response_text = response.text or ""
266
+
267
+ self.memory.append((input_text, response_text))
268
+ self.trim_memory()
269
+
270
+ return response_text
271
+
272
+ def __repr__(self) -> str:
273
+ return (
274
+ f"<{self.__class__.__name__} "
275
+ f"name={self.name!r} "
276
+ f"model={self.model!r} "
277
+ f"memory_window={self.memory_window!r} "
278
+ f"max_turns={self.max_turns!r} "
279
+ f"max_facts={self.max_facts!r}>"
280
+ )