Skip to content

Commit 7791318

Browse files
committed
Release v3.10.18
1 parent e5b5d74 commit 7791318

File tree

20 files changed

+1165
-27
lines changed

20 files changed

+1165
-27
lines changed

docker/Dockerfile.chat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=3.10.17" \
19+
"praisonai>=3.10.18" \
2020
"praisonai[chat]" \
2121
"embedchain[github,youtube]"
2222

docker/Dockerfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
2020
# Install Python packages (using latest versions)
2121
RUN pip install --no-cache-dir \
2222
praisonai_tools \
23-
"praisonai>=3.10.17" \
23+
"praisonai>=3.10.18" \
2424
"praisonai[ui]" \
2525
"praisonai[chat]" \
2626
"praisonai[realtime]" \

docker/Dockerfile.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=3.10.17" \
19+
"praisonai>=3.10.18" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

examples/trace/custom_sink.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""
2+
Custom Trace Sink Example
3+
4+
Demonstrates how to create custom trace sinks for PraisonAI agents.
5+
The trace system uses a protocol-driven design - implement 3 methods
6+
and your sink works with any agent.
7+
8+
Examples:
9+
1. HTTP Sink - Send events to a remote server
10+
2. SQLite Sink - Store events in a database
11+
3. Console Sink - Pretty-print events to terminal
12+
"""
13+
14+
from praisonaiagents import (
15+
Agent,
16+
ContextTraceEmitter,
17+
trace_context,
18+
)
19+
20+
21+
# =============================================================================
22+
# Example 1: HTTP Sink - Send events to a remote server
23+
# =============================================================================
24+
25+
class HTTPSink:
26+
"""Send trace events to a remote HTTP endpoint."""
27+
28+
def __init__(self, url: str, batch_size: int = 10):
29+
self.url = url
30+
self.batch_size = batch_size
31+
self.buffer = []
32+
33+
def emit(self, event):
34+
"""Buffer events and send in batches."""
35+
self.buffer.append(event.to_dict())
36+
if len(self.buffer) >= self.batch_size:
37+
self.flush()
38+
39+
def flush(self):
40+
"""Send buffered events to server."""
41+
if self.buffer:
42+
# In production, use: requests.post(self.url, json=self.buffer)
43+
print(f"[HTTP] Would send {len(self.buffer)} events to {self.url}")
44+
self.buffer.clear()
45+
46+
def close(self):
47+
"""Flush remaining events."""
48+
self.flush()
49+
50+
51+
# =============================================================================
52+
# Example 2: SQLite Sink - Store events in a database
53+
# =============================================================================
54+
55+
class SQLiteSink:
56+
"""Store trace events in SQLite database."""
57+
58+
def __init__(self, db_path: str = "traces.db"):
59+
import sqlite3
60+
self.conn = sqlite3.connect(db_path)
61+
self.conn.execute("""
62+
CREATE TABLE IF NOT EXISTS events (
63+
id INTEGER PRIMARY KEY,
64+
session_id TEXT,
65+
event_type TEXT,
66+
agent_name TEXT,
67+
timestamp REAL,
68+
data TEXT
69+
)
70+
""")
71+
self.conn.commit()
72+
73+
def emit(self, event):
74+
"""Insert event into database."""
75+
import json
76+
self.conn.execute(
77+
"INSERT INTO events (session_id, event_type, agent_name, timestamp, data) VALUES (?, ?, ?, ?, ?)",
78+
(event.session_id, event.event_type.value, event.agent_name, event.timestamp, json.dumps(event.data))
79+
)
80+
81+
def flush(self):
82+
"""Commit pending transactions."""
83+
self.conn.commit()
84+
85+
def close(self):
86+
"""Commit and close connection."""
87+
self.flush()
88+
self.conn.close()
89+
90+
91+
# =============================================================================
92+
# Example 3: Console Sink - Pretty-print events
93+
# =============================================================================
94+
95+
class ConsoleSink:
96+
"""Print trace events to console with colors."""
97+
98+
COLORS = {
99+
"session_start": "\033[92m", # Green
100+
"session_end": "\033[92m",
101+
"agent_start": "\033[94m", # Blue
102+
"agent_end": "\033[94m",
103+
"tool_call_start": "\033[93m", # Yellow
104+
"tool_call_end": "\033[93m",
105+
"llm_request": "\033[95m", # Magenta
106+
"llm_response": "\033[95m",
107+
}
108+
RESET = "\033[0m"
109+
110+
def emit(self, event):
111+
"""Print event with color coding."""
112+
color = self.COLORS.get(event.event_type.value, "")
113+
print(f"{color}[{event.event_type.value}]{self.RESET} {event.agent_name or 'session'}")
114+
115+
def flush(self):
116+
pass
117+
118+
def close(self):
119+
pass
120+
121+
122+
# =============================================================================
123+
# Usage Examples
124+
# =============================================================================
125+
126+
def example_http_sink():
127+
"""Example: Send events to HTTP endpoint."""
128+
print("\n=== HTTP Sink Example ===")
129+
130+
sink = HTTPSink(url="https://my-telemetry.example.com/events")
131+
emitter = ContextTraceEmitter(sink=sink, session_id="http-demo", enabled=True)
132+
133+
with trace_context(emitter):
134+
# Events automatically go to HTTPSink
135+
emitter.session_start({"demo": True})
136+
emitter.agent_start("demo_agent", {"role": "demo"})
137+
emitter.agent_end("demo_agent")
138+
emitter.session_end()
139+
140+
print("HTTP sink example complete")
141+
142+
143+
def example_console_sink():
144+
"""Example: Pretty-print events to console."""
145+
print("\n=== Console Sink Example ===")
146+
147+
sink = ConsoleSink()
148+
emitter = ContextTraceEmitter(sink=sink, session_id="console-demo", enabled=True)
149+
150+
with trace_context(emitter):
151+
emitter.session_start({})
152+
emitter.agent_start("researcher", {"role": "Research Assistant"})
153+
emitter.tool_call_start("researcher", "web_search", {"query": "AI trends"})
154+
emitter.tool_call_end("researcher", "web_search", "Found 10 results", 150.0)
155+
emitter.llm_request("researcher", "gpt-4o-mini", [{"role": "user", "content": "test"}])
156+
emitter.llm_response("researcher", 100, 500, "stop", "Response text")
157+
emitter.agent_end("researcher")
158+
emitter.session_end()
159+
160+
print("Console sink example complete")
161+
162+
163+
def example_with_real_agent():
164+
"""Example: Use custom sink with a real agent.
165+
166+
Note: Requires OPENAI_API_KEY environment variable.
167+
"""
168+
print("\n=== Real Agent with Custom Sink ===")
169+
170+
# Create custom sink
171+
sink = ConsoleSink()
172+
emitter = ContextTraceEmitter(sink=sink, session_id="real-agent-demo", enabled=True)
173+
174+
# Use trace_context to capture all agent events
175+
with trace_context(emitter):
176+
agent = Agent(
177+
name="demo_agent",
178+
instructions="You are a helpful assistant. Be brief.",
179+
llm="gpt-4o-mini",
180+
)
181+
182+
# This chat will emit events to ConsoleSink
183+
# Uncomment to run (requires API key):
184+
# result = agent.chat("Say hello in 5 words or less")
185+
# print(f"Agent response: {result}")
186+
187+
print("Real agent example complete (agent.chat commented out)")
188+
189+
190+
if __name__ == "__main__":
191+
# Run examples
192+
example_http_sink()
193+
example_console_sink()
194+
example_with_real_agent()
195+
196+
print("\n✅ All examples complete!")
197+
print("\nKey points:")
198+
print("1. Implement emit(), flush(), close() - that's it!")
199+
print("2. Use trace_context() for automatic cleanup")
200+
print("3. Zero overhead when tracing is disabled")

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,15 @@ def _get_lazy_cache():
250250
'READ_ONLY_TOOLS': ('praisonaiagents.planning', 'READ_ONLY_TOOLS'),
251251
'RESTRICTED_TOOLS': ('praisonaiagents.planning', 'RESTRICTED_TOOLS'),
252252

253+
# Trace (protocol-driven, for custom sinks)
254+
'ContextTraceSink': ('praisonaiagents.trace', 'ContextTraceSink'),
255+
'ContextTraceEmitter': ('praisonaiagents.trace', 'ContextTraceEmitter'),
256+
'ContextEvent': ('praisonaiagents.trace', 'ContextEvent'),
257+
'ContextEventType': ('praisonaiagents.trace', 'ContextEventType'),
258+
'trace_context': ('praisonaiagents.trace', 'trace_context'),
259+
'ContextListSink': ('praisonaiagents.trace', 'ContextListSink'),
260+
'ContextNoOpSink': ('praisonaiagents.trace', 'ContextNoOpSink'),
261+
253262
# Telemetry
254263
'get_telemetry': ('praisonaiagents.telemetry', 'get_telemetry'),
255264
'enable_telemetry': ('praisonaiagents.telemetry', 'enable_telemetry'),

src/praisonai-agents/praisonaiagents/memory/file_memory.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,24 @@ def _log(self, msg: str, level: int = logging.INFO):
175175
if self.verbose >= 1:
176176
logger.log(level, msg)
177177

178+
def _emit_memory_event(self, event_type: str, memory_type: str,
179+
content_length: int = 0, query: str = "",
180+
result_count: int = 0, top_score: float = None,
181+
metadata: Dict[str, Any] = None):
182+
"""Emit memory trace event if tracing is enabled (zero overhead when disabled)."""
183+
try:
184+
from ..trace.context_events import get_context_emitter
185+
emitter = get_context_emitter()
186+
if not emitter.enabled:
187+
return
188+
agent_name = self.user_id or "unknown"
189+
if event_type == "store":
190+
emitter.memory_store(agent_name, memory_type, content_length, metadata)
191+
elif event_type == "search":
192+
emitter.memory_search(agent_name, query, result_count, memory_type, top_score)
193+
except Exception:
194+
pass # Silent fail - tracing should never break memory operations
195+
178196
def _generate_id(self, content: str) -> str:
179197
"""Generate a unique ID for content."""
180198
timestamp = str(time.time())
@@ -321,6 +339,9 @@ def add_short_term(
321339
self._save_short_term()
322340
self._log(f"Added short-term memory: {content[:50]}...")
323341

342+
# Emit trace event
343+
self._emit_memory_event("store", "short_term", len(content), metadata=metadata)
344+
324345
return item.id
325346

326347
def get_short_term(self, limit: Optional[int] = None) -> List[MemoryItem]:
@@ -387,6 +408,9 @@ def add_long_term(
387408
self._save_long_term()
388409
self._log(f"Added long-term memory: {content[:50]}...")
389410

411+
# Emit trace event
412+
self._emit_memory_event("store", "long_term", len(content), metadata=metadata)
413+
390414
return item.id
391415

392416
def get_long_term(self, limit: Optional[int] = None) -> List[MemoryItem]:
@@ -654,7 +678,14 @@ def score_match(text: str) -> float:
654678

655679
# Sort by score and limit
656680
results.sort(key=lambda x: x["score"], reverse=True)
657-
return results[:limit]
681+
final_results = results[:limit]
682+
683+
# Emit trace event for search
684+
top_score = final_results[0]["score"] if final_results else None
685+
self._emit_memory_event("search", "all", query=query,
686+
result_count=len(final_results), top_score=top_score)
687+
688+
return final_results
658689

659690
def get_context(
660691
self,

src/praisonai-agents/praisonaiagents/trace/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
"ContextNoOpSink",
5252
"ContextListSink",
5353
"ContextTraceEmitter",
54+
# Context manager
55+
"trace_context",
56+
# Global emitter registry
57+
"get_context_emitter",
58+
"set_context_emitter",
59+
"reset_context_emitter",
5460
]
5561

5662

@@ -85,4 +91,12 @@ def __getattr__(name: str):
8591
from .context_events import ContextTraceEmitter
8692
return ContextTraceEmitter
8793

94+
if name == "trace_context":
95+
from .context_events import trace_context
96+
return trace_context
97+
98+
if name in ("get_context_emitter", "set_context_emitter", "reset_context_emitter"):
99+
from .context_events import get_context_emitter, set_context_emitter, reset_context_emitter
100+
return locals()[name]
101+
88102
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

0 commit comments

Comments
 (0)