gateforge-sdk 0.2.3__tar.gz → 0.2.4__tar.gz

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 (40) hide show
  1. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/PKG-INFO +2 -2
  2. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/context.py +29 -9
  3. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/pyproject.toml +2 -2
  4. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/tests/test_phase1.py +57 -31
  5. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/.env.example +0 -0
  6. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/.gitignore +0 -0
  7. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/.pypirc.example +0 -0
  8. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/INSTALL.md +0 -0
  9. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/LICENSE +0 -0
  10. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/MANIFEST.in +0 -0
  11. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/PUBLISHING.md +0 -0
  12. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/PUBLISH_NOW.md +0 -0
  13. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/README.md +0 -0
  14. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/__init__.py +0 -0
  15. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/ab/__init__.py +0 -0
  16. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/ab/engine.py +0 -0
  17. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/client.py +0 -0
  18. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/config.py +0 -0
  19. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/features/__init__.py +0 -0
  20. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/guardrails/__init__.py +0 -0
  21. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/guardrails/engine.py +0 -0
  22. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/metrics.py +0 -0
  23. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/options.py +0 -0
  24. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/pii.py +0 -0
  25. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/pricing.py +0 -0
  26. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/prompt.py +0 -0
  27. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/providers/__init__.py +0 -0
  28. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/providers/anthropic.py +0 -0
  29. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/providers/gemini.py +0 -0
  30. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/providers/openai.py +0 -0
  31. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/response.py +0 -0
  32. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/tracing.py +0 -0
  33. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/wrappers/__init__.py +0 -0
  34. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/wrappers/anthropic.py +0 -0
  35. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/wrappers/gemini.py +0 -0
  36. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/gateforge/wrappers/openai.py +0 -0
  37. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/tests/__init__.py +0 -0
  38. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/tests/test_metrics.py +0 -0
  39. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/tests/test_pii.py +0 -0
  40. {gateforge_sdk-0.2.3 → gateforge_sdk-0.2.4}/tests/test_providers.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gateforge-sdk
3
- Version: 0.2.3
4
- Summary: Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session tracing, PII masking, cost tracking
3
+ Version: 0.2.4
4
+ Summary: Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session tracing, nested conversation support
5
5
  Project-URL: Homepage, https://gateforge.dev
6
6
  Project-URL: Documentation, https://gateforge.dev/docs
7
7
  Project-URL: Repository, https://github.com/gateforge/gateforge-sdk
@@ -183,6 +183,9 @@ def agent(
183
183
  """
184
184
  Decorator that creates a trace context for an agent function.
185
185
 
186
+ If there's an active trace (e.g., from gateforge.session()), it uses that
187
+ conversation_id for proper nesting. Otherwise creates a new trace.
188
+
186
189
  All LLM calls and tool calls inside are automatically traced.
187
190
 
188
191
  Usage::
@@ -190,14 +193,27 @@ def agent(
190
193
  def my_agent(message):
191
194
  response = client.chat.completions.create(...)
192
195
  return response
196
+
197
+ # Or inside a session - shares the session's conversation_id:
198
+ with gateforge.session() as sess:
199
+ my_agent("hello") # Uses sess.conversation_id
193
200
  """
194
201
  def decorator(fn: Callable) -> Callable:
195
202
  @functools.wraps(fn)
196
203
  def wrapper(*args: Any, **kwargs: Any) -> Any:
197
204
  from gateforge.tracing import emit_trace_event
198
205
 
199
- # Generate or use provided conversation_id
200
- cid = conversation_id or f"{fn.__name__}-{int(time.time())}"
206
+ # Check if there's an active trace (e.g., from session())
207
+ active_trace = get_active_trace()
208
+
209
+ if active_trace:
210
+ # Use existing conversation_id for nesting
211
+ cid = active_trace.conversation_id
212
+ is_nested = True
213
+ else:
214
+ # Create new conversation_id
215
+ cid = conversation_id or f"{fn.__name__}-{int(time.time())}"
216
+ is_nested = False
201
217
 
202
218
  # Emit agent start event
203
219
  emit_trace_event(
@@ -206,12 +222,15 @@ def agent(
206
222
  conversation_id=cid,
207
223
  event_type="agent_start",
208
224
  step=0,
209
- metadata={"user_id": user_id, "args": _safe_str(args)[:200], "agent_name": name or fn.__name__},
225
+ metadata={"user_id": user_id, "args": _safe_str(args)[:200], "agent_name": name or fn.__name__, "nested": is_nested},
210
226
  )
211
227
 
212
- # Enter trace context
213
- ctx = trace(conversation_id=cid)
214
- ctx.__enter__()
228
+ # Enter trace context (only if not already in one)
229
+ if not is_nested:
230
+ ctx = trace(conversation_id=cid)
231
+ ctx.__enter__()
232
+ else:
233
+ ctx = None # Already in a trace context
215
234
 
216
235
  try:
217
236
  result = fn(*args, **kwargs)
@@ -222,7 +241,7 @@ def agent(
222
241
  base_url=_get_base_url(),
223
242
  conversation_id=cid,
224
243
  event_type="agent_end",
225
- step=ctx.ctx.step,
244
+ step=active_trace.step if active_trace else ctx.ctx.step,
226
245
  metadata={"success": True},
227
246
  )
228
247
 
@@ -234,12 +253,13 @@ def agent(
234
253
  base_url=_get_base_url(),
235
254
  conversation_id=cid,
236
255
  event_type="agent_error",
237
- step=ctx.ctx.step,
256
+ step=active_trace.step if active_trace else ctx.ctx.step,
238
257
  metadata={"error": str(e)[:500]},
239
258
  )
240
259
  raise
241
260
  finally:
242
- ctx.__exit__(None, None, None)
261
+ if ctx:
262
+ ctx.__exit__(None, None, None)
243
263
 
244
264
  return wrapper
245
265
  return decorator
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gateforge-sdk"
7
- version = "0.2.3"
8
- description = "Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session tracing, PII masking, cost tracking"
7
+ version = "0.2.4"
8
+ description = "Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session tracing, nested conversation support"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
@@ -20,9 +20,7 @@ class TestAutoInit:
20
20
 
21
21
  def test_auto_init_no_api_key(self, capsys):
22
22
  """auto_init() should silently skip if no API key found."""
23
- # Ensure no API key in env
24
23
  with patch.dict(os.environ, {}, clear=True):
25
- # Need to reload module to pick up new env
26
24
  import importlib
27
25
  import gateforge
28
26
  importlib.reload(gateforge)
@@ -36,7 +34,6 @@ class TestAutoInit:
36
34
  def test_auto_init_with_api_key(self, capsys):
37
35
  """auto_init() should initialize successfully with API key."""
38
36
  with patch.dict(os.environ, {"GATEFORGE_API_KEY": "gf_test_key"}):
39
- # Mock the init function to avoid actual API calls
40
37
  with patch('gateforge.init') as mock_init:
41
38
  import gateforge
42
39
  result = gateforge.auto_init()
@@ -45,7 +42,6 @@ class TestAutoInit:
45
42
  captured = capsys.readouterr()
46
43
  assert "Auto-initialized successfully" in captured.out
47
44
 
48
- # Verify init was called with correct params
49
45
  mock_init.assert_called_once()
50
46
  call_kwargs = mock_init.call_args[1]
51
47
  assert call_kwargs['api_key'] == 'gf_test_key'
@@ -107,7 +103,6 @@ class TestToolDecorator:
107
103
  """Tool should emit trace event when inside active trace."""
108
104
  import gateforge
109
105
 
110
- # Mock emit_trace_event in tracing module where it's called
111
106
  with patch('gateforge.tracing.emit_trace_event') as mock_emit:
112
107
  @gateforge.tool()
113
108
  def weather_tool(location: str) -> str:
@@ -118,7 +113,6 @@ class TestToolDecorator:
118
113
 
119
114
  assert result == "Weather in Madrid: sunny"
120
115
 
121
- # Verify trace event was emitted
122
116
  mock_emit.assert_called_once()
123
117
  call_kwargs = mock_emit.call_args[1]
124
118
  assert call_kwargs['conversation_id'] == 'test-123'
@@ -140,7 +134,6 @@ class TestToolDecorator:
140
134
  with pytest.raises(ValueError):
141
135
  failing_tool()
142
136
 
143
- # Verify error was captured
144
137
  mock_emit.assert_called_once()
145
138
  call_kwargs = mock_emit.call_args[1]
146
139
  assert call_kwargs['metadata']['success'] is False
@@ -185,13 +178,12 @@ class TestAgentDecorator:
185
178
  """Tests for @gateforge.agent() decorator."""
186
179
 
187
180
  def test_agent_creates_trace_context(self):
188
- """Agent should create trace context automatically."""
181
+ """Agent should create trace context if not already in one."""
189
182
  import gateforge
190
183
  from gateforge.context import get_active_trace
191
184
 
192
185
  @gateforge.agent(conversation_id="agent-test-1")
193
186
  def my_agent(message: str) -> str:
194
- # Should have active trace inside agent
195
187
  trace_ctx = get_active_trace()
196
188
  assert trace_ctx is not None
197
189
  assert trace_ctx.conversation_id == "agent-test-1"
@@ -200,6 +192,20 @@ class TestAgentDecorator:
200
192
  result = my_agent("Hello")
201
193
  assert "Hello" in result
202
194
 
195
+ def test_agent_uses_active_session_conversation_id(self):
196
+ """Agent should use session's conversation_id when nested."""
197
+ import gateforge
198
+ from gateforge.context import get_active_trace
199
+
200
+ @gateforge.agent()
201
+ def my_agent():
202
+ trace_ctx = get_active_trace()
203
+ return trace_ctx.conversation_id
204
+
205
+ with gateforge.session(conversation_id="session-123") as sess:
206
+ agent_cid = my_agent()
207
+ assert agent_cid == "session-123"
208
+
203
209
  def test_agent_emits_start_end_events(self):
204
210
  """Agent should emit start and end events."""
205
211
  import gateforge
@@ -211,19 +217,33 @@ class TestAgentDecorator:
211
217
 
212
218
  result = my_agent("Hello")
213
219
 
214
- # Should emit 2 events: start and end
215
220
  assert mock_emit.call_count == 2
216
221
 
217
- # First call: agent_start
218
222
  first_call = mock_emit.call_args_list[0][1]
219
223
  assert first_call['event_type'] == 'agent_start'
220
224
  assert first_call['conversation_id'] == 'agent-test-2'
221
225
 
222
- # Second call: agent_end
223
226
  second_call = mock_emit.call_args_list[1][1]
224
227
  assert second_call['event_type'] == 'agent_end'
225
228
  assert second_call['metadata']['success'] is True
226
229
 
230
+ def test_agent_nested_doesnt_create_new_trace(self):
231
+ """Nested agent should not create new trace context."""
232
+ import gateforge
233
+
234
+ with patch('gateforge.tracing.emit_trace_event') as mock_emit:
235
+ @gateforge.agent()
236
+ def my_agent():
237
+ return "OK"
238
+
239
+ with gateforge.session(conversation_id="session-456"):
240
+ my_agent()
241
+
242
+ assert mock_emit.call_count == 4
243
+
244
+ conversation_ids = [call[1]['conversation_id'] for call in mock_emit.call_args_list]
245
+ assert all(cid == 'session-456' for cid in conversation_ids)
246
+
227
247
  def test_agent_emits_error_event(self):
228
248
  """Agent should emit error event on exception."""
229
249
  import gateforge
@@ -236,16 +256,14 @@ class TestAgentDecorator:
236
256
  with pytest.raises(RuntimeError):
237
257
  failing_agent()
238
258
 
239
- # Should emit 2 events: start and error
240
259
  assert mock_emit.call_count == 2
241
260
 
242
- # Second call: agent_error
243
261
  second_call = mock_emit.call_args_list[1][1]
244
262
  assert second_call['event_type'] == 'agent_error'
245
263
  assert 'Agent failed' in second_call['metadata']['error']
246
264
 
247
265
  def test_agent_auto_generates_conversation_id(self):
248
- """Agent should auto-generate conversation_id if not provided."""
266
+ """Agent should auto-generate conversation_id if not provided and no active trace."""
249
267
  import gateforge
250
268
 
251
269
  with patch('gateforge.tracing.emit_trace_event') as mock_emit:
@@ -255,11 +273,27 @@ class TestAgentDecorator:
255
273
 
256
274
  my_agent()
257
275
 
258
- # Verify conversation_id was generated
259
276
  first_call = mock_emit.call_args_list[0][1]
260
277
  assert 'conversation_id' in first_call
261
278
  assert first_call['conversation_id'].startswith('my_agent-')
262
279
 
280
+ def test_agent_nested_shows_metadata(self):
281
+ """Nested agent should include nested=True in metadata."""
282
+ import gateforge
283
+
284
+ with patch('gateforge.tracing.emit_trace_event') as mock_emit:
285
+ @gateforge.agent()
286
+ def my_agent():
287
+ return "OK"
288
+
289
+ with gateforge.session():
290
+ my_agent()
291
+
292
+ for call in mock_emit.call_args_list:
293
+ if call[1]['event_type'] == 'agent_start':
294
+ assert call[1]['metadata']['nested'] is True
295
+ break
296
+
263
297
  def test_agent_with_user_id(self):
264
298
  """Agent should include user_id in metadata."""
265
299
  import gateforge
@@ -287,15 +321,12 @@ class TestSession:
287
321
  assert sess.conversation_id is not None
288
322
  assert sess.user_id == "user-456"
289
323
 
290
- # Should emit start and end events
291
324
  assert mock_emit.call_count == 2
292
325
 
293
- # First: session_start
294
326
  first_call = mock_emit.call_args_list[0][1]
295
327
  assert first_call['event_type'] == 'session_start'
296
328
  assert first_call['metadata']['user_id'] == 'user-456'
297
329
 
298
- # Second: session_end
299
330
  second_call = mock_emit.call_args_list[1][1]
300
331
  assert second_call['event_type'] == 'session_end'
301
332
 
@@ -314,15 +345,12 @@ class TestSession:
314
345
  """Session provides a conversation context that can be used by nested traces."""
315
346
  import gateforge
316
347
 
317
- # Session provides its conversation_id for use
318
348
  with gateforge.session(conversation_id="session-test") as sess:
319
- # Can explicitly continue the session's conversation
320
349
  with gateforge.continue_session(sess.conversation_id) as t1:
321
350
  assert t1.conversation_id == "session-test"
322
351
 
323
- # Or create independent traces (also valid)
324
352
  with gateforge.trace() as t2:
325
- assert t2.conversation_id != "session-test" # Independent trace
353
+ assert t2.conversation_id != "session-test"
326
354
 
327
355
  def test_session_emits_error_on_exception(self):
328
356
  """Session should emit error event on exception."""
@@ -333,10 +361,8 @@ class TestSession:
333
361
  with gateforge.session() as sess:
334
362
  raise ValueError("Session error")
335
363
 
336
- # Should emit start, error, and finally end (3 total)
337
364
  assert mock_emit.call_count >= 2
338
365
 
339
- # Find the error event
340
366
  error_event = None
341
367
  for call in mock_emit.call_args_list:
342
368
  if call[1]['event_type'] == 'session_error':
@@ -351,7 +377,7 @@ class TestIntegration:
351
377
  """Integration tests combining multiple features."""
352
378
 
353
379
  def test_agent_with_tools(self):
354
- """Agent with decorated tools should trace everything."""
380
+ """Agent with decorated tools should trace everything with same conversation_id."""
355
381
  import gateforge
356
382
 
357
383
  with patch('gateforge.tracing.emit_trace_event') as mock_emit:
@@ -366,18 +392,16 @@ class TestIntegration:
366
392
 
367
393
  result = my_agent()
368
394
 
369
- # Should emit: agent_start, tool_call, agent_end
370
395
  assert mock_emit.call_count == 3
371
396
 
372
397
  events = [call[1]['event_type'] for call in mock_emit.call_args_list]
373
398
  assert events == ['agent_start', 'tool_call', 'agent_end']
374
399
 
375
- # All should share same conversation_id
376
400
  conversation_ids = [call[1]['conversation_id'] for call in mock_emit.call_args_list]
377
401
  assert all(cid == 'integration-1' for cid in conversation_ids)
378
402
 
379
403
  def test_session_with_agent_and_tools(self):
380
- """Session wrapping agent with tools should trace everything."""
404
+ """Session wrapping agent with tools should all share same conversation_id."""
381
405
  import gateforge
382
406
 
383
407
  with patch('gateforge.tracing.emit_trace_event') as mock_emit:
@@ -389,10 +413,9 @@ class TestIntegration:
389
413
  def weather_agent(location: str):
390
414
  return get_weather(location)
391
415
 
392
- with gateforge.session(user_id="user-integration"):
416
+ with gateforge.session(conversation_id="session-integration"):
393
417
  result = weather_agent("Madrid")
394
418
 
395
- # Should emit: session_start, agent_start, tool_call, agent_end, session_end
396
419
  assert mock_emit.call_count == 5
397
420
 
398
421
  events = [call[1]['event_type'] for call in mock_emit.call_args_list]
@@ -403,6 +426,9 @@ class TestIntegration:
403
426
  'agent_end',
404
427
  'session_end'
405
428
  ]
429
+
430
+ conversation_ids = [call[1]['conversation_id'] for call in mock_emit.call_args_list]
431
+ assert all(cid == 'session-integration' for cid in conversation_ids), f"Got different IDs: {set(conversation_ids)}"
406
432
 
407
433
 
408
434
  if __name__ == '__main__':
File without changes
File without changes
File without changes
File without changes
File without changes