ouroboros-ai 0.1.0__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.

Potentially problematic release.


This version of ouroboros-ai might be problematic. Click here for more details.

Files changed (81) hide show
  1. ouroboros/__init__.py +15 -0
  2. ouroboros/__main__.py +9 -0
  3. ouroboros/bigbang/__init__.py +39 -0
  4. ouroboros/bigbang/ambiguity.py +464 -0
  5. ouroboros/bigbang/interview.py +530 -0
  6. ouroboros/bigbang/seed_generator.py +610 -0
  7. ouroboros/cli/__init__.py +9 -0
  8. ouroboros/cli/commands/__init__.py +7 -0
  9. ouroboros/cli/commands/config.py +79 -0
  10. ouroboros/cli/commands/init.py +425 -0
  11. ouroboros/cli/commands/run.py +201 -0
  12. ouroboros/cli/commands/status.py +85 -0
  13. ouroboros/cli/formatters/__init__.py +31 -0
  14. ouroboros/cli/formatters/panels.py +157 -0
  15. ouroboros/cli/formatters/progress.py +112 -0
  16. ouroboros/cli/formatters/tables.py +166 -0
  17. ouroboros/cli/main.py +60 -0
  18. ouroboros/config/__init__.py +81 -0
  19. ouroboros/config/loader.py +292 -0
  20. ouroboros/config/models.py +332 -0
  21. ouroboros/core/__init__.py +62 -0
  22. ouroboros/core/ac_tree.py +401 -0
  23. ouroboros/core/context.py +472 -0
  24. ouroboros/core/errors.py +246 -0
  25. ouroboros/core/seed.py +212 -0
  26. ouroboros/core/types.py +205 -0
  27. ouroboros/evaluation/__init__.py +110 -0
  28. ouroboros/evaluation/consensus.py +350 -0
  29. ouroboros/evaluation/mechanical.py +351 -0
  30. ouroboros/evaluation/models.py +235 -0
  31. ouroboros/evaluation/pipeline.py +286 -0
  32. ouroboros/evaluation/semantic.py +302 -0
  33. ouroboros/evaluation/trigger.py +278 -0
  34. ouroboros/events/__init__.py +5 -0
  35. ouroboros/events/base.py +80 -0
  36. ouroboros/events/decomposition.py +153 -0
  37. ouroboros/events/evaluation.py +248 -0
  38. ouroboros/execution/__init__.py +44 -0
  39. ouroboros/execution/atomicity.py +451 -0
  40. ouroboros/execution/decomposition.py +481 -0
  41. ouroboros/execution/double_diamond.py +1386 -0
  42. ouroboros/execution/subagent.py +275 -0
  43. ouroboros/observability/__init__.py +63 -0
  44. ouroboros/observability/drift.py +383 -0
  45. ouroboros/observability/logging.py +504 -0
  46. ouroboros/observability/retrospective.py +338 -0
  47. ouroboros/orchestrator/__init__.py +78 -0
  48. ouroboros/orchestrator/adapter.py +391 -0
  49. ouroboros/orchestrator/events.py +278 -0
  50. ouroboros/orchestrator/runner.py +597 -0
  51. ouroboros/orchestrator/session.py +486 -0
  52. ouroboros/persistence/__init__.py +23 -0
  53. ouroboros/persistence/checkpoint.py +511 -0
  54. ouroboros/persistence/event_store.py +183 -0
  55. ouroboros/persistence/migrations/__init__.py +1 -0
  56. ouroboros/persistence/migrations/runner.py +100 -0
  57. ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
  58. ouroboros/persistence/schema.py +56 -0
  59. ouroboros/persistence/uow.py +230 -0
  60. ouroboros/providers/__init__.py +28 -0
  61. ouroboros/providers/base.py +133 -0
  62. ouroboros/providers/claude_code_adapter.py +212 -0
  63. ouroboros/providers/litellm_adapter.py +316 -0
  64. ouroboros/py.typed +0 -0
  65. ouroboros/resilience/__init__.py +67 -0
  66. ouroboros/resilience/lateral.py +595 -0
  67. ouroboros/resilience/stagnation.py +727 -0
  68. ouroboros/routing/__init__.py +60 -0
  69. ouroboros/routing/complexity.py +272 -0
  70. ouroboros/routing/downgrade.py +664 -0
  71. ouroboros/routing/escalation.py +340 -0
  72. ouroboros/routing/router.py +204 -0
  73. ouroboros/routing/tiers.py +247 -0
  74. ouroboros/secondary/__init__.py +40 -0
  75. ouroboros/secondary/scheduler.py +467 -0
  76. ouroboros/secondary/todo_registry.py +483 -0
  77. ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
  78. ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
  79. ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
  80. ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
  81. ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,504 @@
1
+ """Structured logging configuration for Ouroboros.
2
+
3
+ This module configures structlog with standard processors for structured
4
+ logging throughout the application. It supports both development mode
5
+ (human-readable console output) and production mode (JSON output).
6
+
7
+ Features:
8
+ - ISO 8601 timestamps
9
+ - Log level in all entries
10
+ - contextvars integration for cross-async context propagation
11
+ - Daily log rotation with configurable retention
12
+ - Mode selection via environment variable or config
13
+
14
+ Standard log keys:
15
+ - seed_id: Seed identifier
16
+ - ac_id: Atomic Capability identifier
17
+ - depth: Current depth in execution tree
18
+ - iteration: Iteration number
19
+ - tier: PAL routing tier
20
+
21
+ Event naming convention:
22
+ - Use dot.notation (e.g., "ac.execution.started", "ontology.concept.added")
23
+ - Format: domain.entity.verb_past_tense
24
+
25
+ Usage:
26
+ from ouroboros.observability import configure_logging, get_logger, bind_context
27
+
28
+ # Configure at application startup
29
+ configure_logging(LoggingConfig(mode=LogMode.DEV))
30
+
31
+ # Get a logger
32
+ log = get_logger()
33
+
34
+ # Bind context for async propagation
35
+ bind_context(seed_id="seed_123", ac_id="ac_456")
36
+
37
+ # Log with standard keys
38
+ log.info("ac.execution.started", depth=2, iteration=1, tier="mini")
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from enum import Enum
44
+ import logging
45
+ from logging.handlers import TimedRotatingFileHandler
46
+ import os
47
+ from pathlib import Path
48
+ from typing import Any
49
+
50
+ from pydantic import BaseModel, Field
51
+ import structlog
52
+
53
+
54
+ class LogMode(str, Enum):
55
+ """Logging output mode."""
56
+
57
+ DEV = "dev"
58
+ PROD = "prod"
59
+
60
+
61
+ class LoggingConfig(BaseModel):
62
+ """Configuration for structured logging.
63
+
64
+ Attributes:
65
+ mode: Output mode (dev for human-readable, prod for JSON).
66
+ log_level: Minimum log level to output.
67
+ log_dir: Directory for log files. Defaults to ~/.ouroboros/logs/.
68
+ max_log_days: Number of days to retain log files. Defaults to 7.
69
+ enable_file_logging: Whether to write logs to files.
70
+ """
71
+
72
+ mode: LogMode = Field(default=LogMode.DEV)
73
+ log_level: str = Field(default="INFO")
74
+ log_dir: Path = Field(default_factory=lambda: Path.home() / ".ouroboros" / "logs")
75
+ max_log_days: int = Field(default=7, ge=1, le=365)
76
+ enable_file_logging: bool = Field(default=True)
77
+
78
+ model_config = {"frozen": True}
79
+
80
+
81
+ # Module-level state for tracking configuration
82
+ _configured: bool = False
83
+ _current_config: LoggingConfig | None = None
84
+
85
+
86
+ def _get_mode_from_env() -> LogMode:
87
+ """Get logging mode from environment variable.
88
+
89
+ Returns:
90
+ LogMode based on OUROBOROS_LOG_MODE environment variable.
91
+ Defaults to DEV if not set or invalid.
92
+ """
93
+ env_mode = os.environ.get("OUROBOROS_LOG_MODE", "dev").lower()
94
+ if env_mode == "prod":
95
+ return LogMode.PROD
96
+ return LogMode.DEV
97
+
98
+
99
+ def _get_log_level(level_str: str) -> int:
100
+ """Convert log level string to logging constant.
101
+
102
+ Args:
103
+ level_str: Log level as string (e.g., "INFO", "DEBUG").
104
+
105
+ Returns:
106
+ Logging constant (e.g., logging.INFO).
107
+ """
108
+ level_map = {
109
+ "DEBUG": logging.DEBUG,
110
+ "INFO": logging.INFO,
111
+ "WARNING": logging.WARNING,
112
+ "WARN": logging.WARNING,
113
+ "ERROR": logging.ERROR,
114
+ "CRITICAL": logging.CRITICAL,
115
+ }
116
+ return level_map.get(level_str.upper(), logging.INFO)
117
+
118
+
119
+ def _setup_file_handler(config: LoggingConfig) -> TimedRotatingFileHandler | None:
120
+ """Set up rotating file handler for log output.
121
+
122
+ Args:
123
+ config: Logging configuration.
124
+
125
+ Returns:
126
+ Configured TimedRotatingFileHandler or None if file logging disabled.
127
+ """
128
+ if not config.enable_file_logging:
129
+ return None
130
+
131
+ # Ensure log directory exists
132
+ config.log_dir.mkdir(parents=True, exist_ok=True)
133
+
134
+ # Create log file path with date
135
+ log_file = config.log_dir / "ouroboros.log"
136
+
137
+ # Configure rotating file handler
138
+ # - when="midnight" for daily rotation
139
+ # - backupCount controls retention
140
+ handler = TimedRotatingFileHandler(
141
+ filename=str(log_file),
142
+ when="midnight",
143
+ interval=1,
144
+ backupCount=config.max_log_days,
145
+ encoding="utf-8",
146
+ utc=True,
147
+ )
148
+
149
+ # Set formatter based on mode
150
+ if config.mode == LogMode.PROD:
151
+ # JSON format for production - structlog will handle formatting
152
+ handler.setFormatter(logging.Formatter("%(message)s"))
153
+ else:
154
+ # Simple format for dev - structlog console renderer handles formatting
155
+ handler.setFormatter(logging.Formatter("%(message)s"))
156
+
157
+ handler.setLevel(_get_log_level(config.log_level))
158
+
159
+ return handler
160
+
161
+
162
+ def _get_shared_processors() -> list[Any]:
163
+ """Get the shared processor chain for structlog.
164
+
165
+ These processors are used for preparing event dicts before final rendering.
166
+
167
+ Returns:
168
+ List of structlog processors.
169
+ """
170
+ return [
171
+ # Merge contextvars into event dict (for cross-async context)
172
+ structlog.contextvars.merge_contextvars,
173
+ # Add log level to all entries
174
+ structlog.processors.add_log_level,
175
+ # Add timestamp in ISO 8601 format
176
+ structlog.processors.TimeStamper(fmt="iso", utc=True),
177
+ # Add stack info for exceptions
178
+ structlog.processors.StackInfoRenderer(),
179
+ # Add caller info (file, line, function) - useful for debugging
180
+ structlog.processors.CallsiteParameterAdder(
181
+ parameters=[
182
+ structlog.processors.CallsiteParameter.FILENAME,
183
+ structlog.processors.CallsiteParameter.LINENO,
184
+ ]
185
+ ),
186
+ ]
187
+
188
+
189
+ def _get_console_processors(mode: LogMode) -> list[Any]:
190
+ """Get the processor chain for console output.
191
+
192
+ Args:
193
+ mode: Logging output mode.
194
+
195
+ Returns:
196
+ List of structlog processors including renderer.
197
+ """
198
+ processors = _get_shared_processors()
199
+
200
+ # Format exceptions nicely for console
201
+ processors.append(structlog.processors.format_exc_info)
202
+
203
+ # Add final renderer based on mode
204
+ if mode == LogMode.DEV:
205
+ # Human-readable console output for development
206
+ processors.append(structlog.dev.ConsoleRenderer(colors=True))
207
+ else:
208
+ # JSON output for production
209
+ processors.append(structlog.processors.JSONRenderer())
210
+
211
+ return processors
212
+
213
+
214
+ def _get_file_processors() -> list[Any]:
215
+ """Get the processor chain for file output.
216
+
217
+ File output always uses JSON format for easy parsing.
218
+
219
+ Returns:
220
+ List of structlog processors for file logging.
221
+ """
222
+ processors = _get_shared_processors()
223
+
224
+ # Format exceptions for file
225
+ processors.append(structlog.processors.format_exc_info)
226
+
227
+ # Always use JSON for file output (for log aggregation tools)
228
+ processors.append(structlog.processors.JSONRenderer())
229
+
230
+ return processors
231
+
232
+
233
+ class _FileWritingPrintLogger:
234
+ """Print logger that also writes to a file handler.
235
+
236
+ This logger writes to stdout (for console output) and optionally
237
+ to a file handler for persistent logging. Supports proper log levels.
238
+ """
239
+
240
+ def __init__(self, file_handler: TimedRotatingFileHandler | None = None) -> None:
241
+ """Initialize the file-writing print logger.
242
+
243
+ Args:
244
+ file_handler: Optional file handler for persistent logging.
245
+ """
246
+ self._file_handler = file_handler
247
+
248
+ def _log(self, message: str, level: int = logging.INFO) -> None:
249
+ """Log a message to console and file with proper level.
250
+
251
+ Args:
252
+ message: The message to log.
253
+ level: The log level (e.g., logging.DEBUG, logging.INFO).
254
+ """
255
+ # Print to console
256
+ print(message)
257
+
258
+ # Write to file if handler exists
259
+ if self._file_handler:
260
+ record = logging.LogRecord(
261
+ name="ouroboros",
262
+ level=level,
263
+ pathname="",
264
+ lineno=0,
265
+ msg=message,
266
+ args=(),
267
+ exc_info=None,
268
+ )
269
+ self._file_handler.emit(record)
270
+
271
+ def msg(self, message: str) -> None:
272
+ """Log a message to console and file (default INFO level)."""
273
+ self._log(message, logging.INFO)
274
+
275
+ # Alias for structlog compatibility
276
+ def __call__(self, message: str) -> None:
277
+ """Log a message (alias for msg)."""
278
+ self.msg(message)
279
+
280
+ # Level-specific methods
281
+ def debug(self, message: str) -> None:
282
+ """Log a debug message."""
283
+ self._log(message, logging.DEBUG)
284
+
285
+ def info(self, message: str) -> None:
286
+ """Log an info message."""
287
+ self._log(message, logging.INFO)
288
+
289
+ def warning(self, message: str) -> None:
290
+ """Log a warning message."""
291
+ self._log(message, logging.WARNING)
292
+
293
+ def warn(self, message: str) -> None:
294
+ """Log a warning message (alias for warning)."""
295
+ self._log(message, logging.WARNING)
296
+
297
+ def error(self, message: str) -> None:
298
+ """Log an error message."""
299
+ self._log(message, logging.ERROR)
300
+
301
+ def critical(self, message: str) -> None:
302
+ """Log a critical message."""
303
+ self._log(message, logging.CRITICAL)
304
+
305
+ def fatal(self, message: str) -> None:
306
+ """Log a fatal message (alias for critical)."""
307
+ self._log(message, logging.CRITICAL)
308
+
309
+ def exception(self, message: str) -> None:
310
+ """Log an exception message (ERROR level)."""
311
+ self._log(message, logging.ERROR)
312
+
313
+
314
+ class _FileWritingPrintLoggerFactory:
315
+ """Factory for creating file-writing print loggers."""
316
+
317
+ def __init__(self, file_handler: TimedRotatingFileHandler | None = None) -> None:
318
+ """Initialize the factory.
319
+
320
+ Args:
321
+ file_handler: Optional file handler for persistent logging.
322
+ """
323
+ self._file_handler = file_handler
324
+
325
+ def __call__(self, *_args: Any) -> _FileWritingPrintLogger:
326
+ """Create a new logger instance.
327
+
328
+ Args:
329
+ *_args: Ignored arguments (structlog may pass logger name).
330
+ """
331
+ return _FileWritingPrintLogger(self._file_handler)
332
+
333
+
334
+ def configure_logging(config: LoggingConfig | None = None) -> None:
335
+ """Configure structlog for the application.
336
+
337
+ This should be called once at application startup. It configures:
338
+ - structlog processors (log level, timestamp, stack info)
339
+ - Output renderer (console for dev, JSON for prod)
340
+ - File handler with daily rotation (if enabled)
341
+ - contextvars integration for cross-async context
342
+
343
+ Args:
344
+ config: Logging configuration. If None, uses defaults with
345
+ mode from OUROBOROS_LOG_MODE environment variable.
346
+
347
+ Example:
348
+ # Use environment variable for mode
349
+ configure_logging()
350
+
351
+ # Or specify config explicitly
352
+ configure_logging(LoggingConfig(mode=LogMode.PROD, max_log_days=14))
353
+ """
354
+ global _configured, _current_config
355
+
356
+ if config is None:
357
+ config = LoggingConfig(mode=_get_mode_from_env())
358
+
359
+ _current_config = config
360
+
361
+ # Set up standard library logging
362
+ log_level = _get_log_level(config.log_level)
363
+
364
+ # Configure root logger
365
+ root_logger = logging.getLogger()
366
+ root_logger.setLevel(log_level)
367
+
368
+ # Remove existing handlers to avoid duplicates on reconfigure
369
+ for handler in root_logger.handlers[:]:
370
+ root_logger.removeHandler(handler)
371
+
372
+ # Add file handler if enabled
373
+ file_handler = _setup_file_handler(config)
374
+ if file_handler:
375
+ root_logger.addHandler(file_handler)
376
+
377
+ # Get processors for console output
378
+ processors = _get_console_processors(config.mode)
379
+
380
+ # Create logger factory that writes to both console and file
381
+ logger_factory = _FileWritingPrintLoggerFactory(file_handler)
382
+
383
+ # Configure structlog
384
+ structlog.configure(
385
+ processors=processors,
386
+ wrapper_class=structlog.make_filtering_bound_logger(log_level),
387
+ context_class=dict,
388
+ logger_factory=logger_factory,
389
+ cache_logger_on_first_use=True,
390
+ )
391
+
392
+ _configured = True
393
+
394
+
395
+ def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
396
+ """Get a bound logger instance.
397
+
398
+ If logging has not been configured, this will configure it with defaults.
399
+
400
+ Args:
401
+ name: Optional logger name. If not provided, uses the calling module.
402
+
403
+ Returns:
404
+ A bound structlog logger.
405
+
406
+ Example:
407
+ log = get_logger()
408
+ log.info("ac.execution.started", seed_id="seed_123", ac_id="ac_456")
409
+ """
410
+ global _configured
411
+
412
+ if not _configured:
413
+ configure_logging()
414
+
415
+ return structlog.get_logger(name)
416
+
417
+
418
+ def bind_context(**kwargs: Any) -> None:
419
+ """Bind context variables for cross-async propagation.
420
+
421
+ Context bound here will be included in all subsequent log entries
422
+ within the same async context. This is useful for propagating
423
+ request-scoped data like seed_id, ac_id, etc.
424
+
425
+ Standard keys:
426
+ - seed_id: Seed identifier
427
+ - ac_id: Atomic Capability identifier
428
+ - depth: Current depth in execution tree
429
+ - iteration: Iteration number
430
+ - tier: PAL routing tier
431
+
432
+ IMPORTANT: Never bind sensitive data (API keys, credentials).
433
+
434
+ Args:
435
+ **kwargs: Key-value pairs to bind to the logging context.
436
+
437
+ Example:
438
+ bind_context(seed_id="seed_123", ac_id="ac_456", depth=2)
439
+
440
+ # All subsequent logs will include these keys
441
+ log.info("ac.execution.started") # Will include seed_id, ac_id, depth
442
+ """
443
+ structlog.contextvars.bind_contextvars(**kwargs)
444
+
445
+
446
+ def unbind_context(*keys: str) -> None:
447
+ """Remove context variables from the logging context.
448
+
449
+ Args:
450
+ *keys: Keys to remove from the context.
451
+
452
+ Example:
453
+ unbind_context("ac_id", "depth")
454
+ """
455
+ structlog.contextvars.unbind_contextvars(*keys)
456
+
457
+
458
+ def clear_context() -> None:
459
+ """Clear all bound context variables.
460
+
461
+ This should be called at the end of a request or execution scope
462
+ to prevent context leakage.
463
+
464
+ Example:
465
+ try:
466
+ bind_context(seed_id="seed_123")
467
+ # ... do work ...
468
+ finally:
469
+ clear_context()
470
+ """
471
+ structlog.contextvars.clear_contextvars()
472
+
473
+
474
+ def get_current_config() -> LoggingConfig | None:
475
+ """Get the current logging configuration.
476
+
477
+ Returns:
478
+ The current LoggingConfig or None if not configured.
479
+ """
480
+ return _current_config
481
+
482
+
483
+ def is_configured() -> bool:
484
+ """Check if logging has been configured.
485
+
486
+ Returns:
487
+ True if configure_logging has been called.
488
+ """
489
+ return _configured
490
+
491
+
492
+ def reset_logging() -> None:
493
+ """Reset logging configuration state.
494
+
495
+ This is primarily for testing purposes. It resets the module state
496
+ but does not reconfigure the loggers.
497
+ """
498
+ global _configured, _current_config
499
+ _configured = False
500
+ _current_config = None
501
+ # Clear any bound context
502
+ structlog.contextvars.clear_contextvars()
503
+ # Reset structlog configuration
504
+ structlog.reset_defaults()