alma-memory 0.4.0__py3-none-any.whl → 0.5.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.
Files changed (77) hide show
  1. alma/__init__.py +88 -44
  2. alma/confidence/__init__.py +1 -1
  3. alma/confidence/engine.py +92 -58
  4. alma/confidence/types.py +34 -14
  5. alma/config/loader.py +3 -2
  6. alma/consolidation/__init__.py +23 -0
  7. alma/consolidation/engine.py +678 -0
  8. alma/consolidation/prompts.py +84 -0
  9. alma/core.py +15 -15
  10. alma/domains/__init__.py +6 -6
  11. alma/domains/factory.py +12 -9
  12. alma/domains/schemas.py +17 -3
  13. alma/domains/types.py +8 -4
  14. alma/events/__init__.py +75 -0
  15. alma/events/emitter.py +284 -0
  16. alma/events/storage_mixin.py +246 -0
  17. alma/events/types.py +126 -0
  18. alma/events/webhook.py +425 -0
  19. alma/exceptions.py +49 -0
  20. alma/extraction/__init__.py +31 -0
  21. alma/extraction/auto_learner.py +264 -0
  22. alma/extraction/extractor.py +420 -0
  23. alma/graph/__init__.py +81 -0
  24. alma/graph/backends/__init__.py +18 -0
  25. alma/graph/backends/memory.py +236 -0
  26. alma/graph/backends/neo4j.py +417 -0
  27. alma/graph/base.py +159 -0
  28. alma/graph/extraction.py +198 -0
  29. alma/graph/store.py +860 -0
  30. alma/harness/__init__.py +4 -4
  31. alma/harness/base.py +18 -9
  32. alma/harness/domains.py +27 -11
  33. alma/initializer/__init__.py +1 -1
  34. alma/initializer/initializer.py +51 -43
  35. alma/initializer/types.py +25 -17
  36. alma/integration/__init__.py +9 -9
  37. alma/integration/claude_agents.py +10 -10
  38. alma/integration/helena.py +32 -22
  39. alma/integration/victor.py +57 -33
  40. alma/learning/__init__.py +27 -27
  41. alma/learning/forgetting.py +198 -148
  42. alma/learning/heuristic_extractor.py +40 -24
  43. alma/learning/protocols.py +62 -14
  44. alma/learning/validation.py +7 -2
  45. alma/mcp/__init__.py +4 -4
  46. alma/mcp/__main__.py +2 -1
  47. alma/mcp/resources.py +17 -16
  48. alma/mcp/server.py +102 -44
  49. alma/mcp/tools.py +174 -37
  50. alma/progress/__init__.py +3 -3
  51. alma/progress/tracker.py +26 -20
  52. alma/progress/types.py +8 -12
  53. alma/py.typed +0 -0
  54. alma/retrieval/__init__.py +11 -11
  55. alma/retrieval/cache.py +20 -21
  56. alma/retrieval/embeddings.py +4 -4
  57. alma/retrieval/engine.py +114 -35
  58. alma/retrieval/scoring.py +73 -63
  59. alma/session/__init__.py +2 -2
  60. alma/session/manager.py +5 -5
  61. alma/session/types.py +5 -4
  62. alma/storage/__init__.py +41 -0
  63. alma/storage/azure_cosmos.py +101 -31
  64. alma/storage/base.py +157 -4
  65. alma/storage/chroma.py +1443 -0
  66. alma/storage/file_based.py +56 -20
  67. alma/storage/pinecone.py +1080 -0
  68. alma/storage/postgresql.py +1452 -0
  69. alma/storage/qdrant.py +1306 -0
  70. alma/storage/sqlite_local.py +376 -31
  71. alma/types.py +62 -14
  72. alma_memory-0.5.0.dist-info/METADATA +905 -0
  73. alma_memory-0.5.0.dist-info/RECORD +76 -0
  74. {alma_memory-0.4.0.dist-info → alma_memory-0.5.0.dist-info}/WHEEL +1 -1
  75. alma_memory-0.4.0.dist-info/METADATA +0 -488
  76. alma_memory-0.4.0.dist-info/RECORD +0 -52
  77. {alma_memory-0.4.0.dist-info → alma_memory-0.5.0.dist-info}/top_level.txt +0 -0
@@ -4,15 +4,15 @@ ALMA Heuristic Extraction.
4
4
  Analyzes outcomes to identify patterns and create heuristics.
5
5
  """
6
6
 
7
- import uuid
8
7
  import logging
9
- from datetime import datetime, timezone
10
- from typing import Optional, List, Dict, Any, Tuple
11
- from dataclasses import dataclass, field
8
+ import uuid
12
9
  from collections import defaultdict
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from typing import Any, Dict, List, Optional, Tuple
13
13
 
14
- from alma.types import Heuristic, Outcome, MemoryScope
15
14
  from alma.storage.base import StorageBackend
15
+ from alma.types import Heuristic, MemoryScope, Outcome
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -20,6 +20,7 @@ logger = logging.getLogger(__name__)
20
20
  @dataclass
21
21
  class PatternCandidate:
22
22
  """A potential pattern for heuristic creation."""
23
+
23
24
  task_type: str
24
25
  strategy: str
25
26
  occurrence_count: int
@@ -56,6 +57,7 @@ class PatternCandidate:
56
57
  @dataclass
57
58
  class ExtractionResult:
58
59
  """Result of heuristic extraction."""
60
+
59
61
  heuristics_created: int = 0
60
62
  heuristics_updated: int = 0
61
63
  patterns_analyzed: int = 0
@@ -137,7 +139,7 @@ class HeuristicExtractor:
137
139
  # Group outcomes by agent and task type
138
140
  grouped = self._group_outcomes(outcomes)
139
141
 
140
- for (ag, task_type), type_outcomes in grouped.items():
142
+ for (ag, _task_type), type_outcomes in grouped.items():
141
143
  # Find patterns within this group
142
144
  patterns = self._identify_patterns(type_outcomes)
143
145
  result.patterns_analyzed += len(patterns)
@@ -204,14 +206,16 @@ class HeuristicExtractor:
204
206
  patterns = []
205
207
  for strategy, group_outcomes in strategy_groups.items():
206
208
  success_count = sum(1 for o in group_outcomes if o.success)
207
- patterns.append(PatternCandidate(
208
- task_type=group_outcomes[0].task_type,
209
- strategy=strategy,
210
- occurrence_count=len(group_outcomes),
211
- success_count=success_count,
212
- failure_count=len(group_outcomes) - success_count,
213
- outcomes=group_outcomes,
214
- ))
209
+ patterns.append(
210
+ PatternCandidate(
211
+ task_type=group_outcomes[0].task_type,
212
+ strategy=strategy,
213
+ occurrence_count=len(group_outcomes),
214
+ success_count=success_count,
215
+ failure_count=len(group_outcomes) - success_count,
216
+ outcomes=group_outcomes,
217
+ )
218
+ )
215
219
 
216
220
  return patterns
217
221
 
@@ -291,8 +295,9 @@ class HeuristicExtractor:
291
295
  )
292
296
 
293
297
  for h in heuristics:
294
- if (task_type in h.condition and
295
- self._strategies_similar(h.strategy, strategy)):
298
+ if task_type in h.condition and self._strategies_similar(
299
+ h.strategy, strategy
300
+ ):
296
301
  return h
297
302
 
298
303
  return None
@@ -305,13 +310,9 @@ class HeuristicExtractor:
305
310
  """Update an existing heuristic with new data."""
306
311
  # Merge counts
307
312
  heuristic.occurrence_count = max(
308
- heuristic.occurrence_count,
309
- pattern.occurrence_count
310
- )
311
- heuristic.success_count = max(
312
- heuristic.success_count,
313
- pattern.success_count
313
+ heuristic.occurrence_count, pattern.occurrence_count
314
314
  )
315
+ heuristic.success_count = max(heuristic.success_count, pattern.success_count)
315
316
 
316
317
  # Update confidence
317
318
  heuristic.confidence = pattern.confidence
@@ -344,8 +345,23 @@ class HeuristicExtractor:
344
345
  """Normalize strategy text for comparison."""
345
346
  # Remove common stop words and normalize
346
347
  stop_words = {
347
- "the", "a", "an", "and", "or", "but", "in", "on", "at",
348
- "to", "for", "of", "with", "by", "then", "first", "next",
348
+ "the",
349
+ "a",
350
+ "an",
351
+ "and",
352
+ "or",
353
+ "but",
354
+ "in",
355
+ "on",
356
+ "at",
357
+ "to",
358
+ "for",
359
+ "of",
360
+ "with",
361
+ "by",
362
+ "then",
363
+ "first",
364
+ "next",
349
365
  }
350
366
 
351
367
  words = strategy.lower().replace(",", " ").replace(".", " ").split()
@@ -4,20 +4,23 @@ ALMA Learning Protocols.
4
4
  Defines how agents learn from outcomes while respecting scope constraints.
5
5
  """
6
6
 
7
- import uuid
8
7
  import logging
9
- from datetime import datetime, timezone, timedelta
10
- from typing import Optional, Dict, Any
8
+ import uuid
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import TYPE_CHECKING, Dict, Optional
11
11
 
12
+ from alma.storage.base import StorageBackend
12
13
  from alma.types import (
14
+ AntiPattern,
15
+ DomainKnowledge,
13
16
  Heuristic,
17
+ MemoryScope,
14
18
  Outcome,
15
19
  UserPreference,
16
- DomainKnowledge,
17
- AntiPattern,
18
- MemoryScope,
19
20
  )
20
- from alma.storage.base import StorageBackend
21
+
22
+ if TYPE_CHECKING:
23
+ from alma.retrieval.embeddings import EmbeddingProvider
21
24
 
22
25
  logger = logging.getLogger(__name__)
23
26
 
@@ -36,6 +39,8 @@ class LearningProtocol:
36
39
  self,
37
40
  storage: StorageBackend,
38
41
  scopes: Dict[str, MemoryScope],
42
+ embedder: Optional["EmbeddingProvider"] = None,
43
+ similarity_threshold: float = 0.75,
39
44
  ):
40
45
  """
41
46
  Initialize learning protocol.
@@ -43,9 +48,13 @@ class LearningProtocol:
43
48
  Args:
44
49
  storage: Storage backend for persistence
45
50
  scopes: Dict of agent_name -> MemoryScope
51
+ embedder: Optional embedding provider for semantic similarity
52
+ similarity_threshold: Cosine similarity threshold for strategy matching (default 0.75)
46
53
  """
47
54
  self.storage = storage
48
55
  self.scopes = scopes
56
+ self.embedder = embedder
57
+ self.similarity_threshold = similarity_threshold
49
58
 
50
59
  def learn(
51
60
  self,
@@ -100,7 +109,9 @@ class LearningProtocol:
100
109
 
101
110
  # Save outcome
102
111
  self.storage.save_outcome(outcome_record)
103
- logger.info(f"Recorded outcome for {agent}: {'success' if outcome else 'failure'}")
112
+ logger.info(
113
+ f"Recorded outcome for {agent}: {'success' if outcome else 'failure'}"
114
+ )
104
115
 
105
116
  # Check if we should create/update a heuristic
106
117
  self._maybe_create_heuristic(
@@ -153,7 +164,8 @@ class LearningProtocol:
153
164
 
154
165
  # Filter to same strategy
155
166
  same_strategy = [
156
- o for o in similar_outcomes
167
+ o
168
+ for o in similar_outcomes
157
169
  if self._strategies_similar(o.strategy_used, strategy)
158
170
  ]
159
171
 
@@ -200,9 +212,11 @@ class LearningProtocol:
200
212
 
201
213
  # Filter to failures with similar error
202
214
  similar = [
203
- o for o in similar_failures
204
- if not o.success and o.error_message and
205
- self._errors_similar(o.error_message, error)
215
+ o
216
+ for o in similar_failures
217
+ if not o.success
218
+ and o.error_message
219
+ and self._errors_similar(o.error_message, error)
206
220
  ]
207
221
 
208
222
  if len(similar) >= 2: # At least 2 similar failures
@@ -311,13 +325,47 @@ class LearningProtocol:
311
325
  return "general"
312
326
 
313
327
  def _strategies_similar(self, s1: str, s2: str) -> bool:
314
- """Check if two strategies are similar enough to count together."""
315
- # Simple word overlap check - could be improved with embeddings
328
+ """
329
+ Check if two strategies are similar enough to count together.
330
+
331
+ Uses embedding-based cosine similarity when an embedder is available,
332
+ otherwise falls back to simple word overlap.
333
+ """
334
+ if self.embedder is not None:
335
+ return self._strategies_similar_embedding(s1, s2)
336
+ return self._strategies_similar_word_overlap(s1, s2)
337
+
338
+ def _strategies_similar_embedding(self, s1: str, s2: str) -> bool:
339
+ """Check strategy similarity using embedding cosine similarity."""
340
+ try:
341
+ emb1 = self.embedder.encode(s1)
342
+ emb2 = self.embedder.encode(s2)
343
+ similarity = self._cosine_similarity(emb1, emb2)
344
+ return similarity >= self.similarity_threshold
345
+ except Exception as e:
346
+ logger.warning(
347
+ f"Embedding similarity failed, falling back to word overlap: {e}"
348
+ )
349
+ return self._strategies_similar_word_overlap(s1, s2)
350
+
351
+ def _strategies_similar_word_overlap(self, s1: str, s2: str) -> bool:
352
+ """Check strategy similarity using simple word overlap."""
316
353
  words1 = set(s1.lower().split())
317
354
  words2 = set(s2.lower().split())
318
355
  overlap = len(words1 & words2)
319
356
  return overlap >= min(3, len(words1) // 2)
320
357
 
358
+ def _cosine_similarity(self, v1: list, v2: list) -> float:
359
+ """Compute cosine similarity between two vectors."""
360
+ import math
361
+
362
+ dot_product = sum(a * b for a, b in zip(v1, v2, strict=False))
363
+ norm1 = math.sqrt(sum(a * a for a in v1))
364
+ norm2 = math.sqrt(sum(b * b for b in v2))
365
+ if norm1 == 0 or norm2 == 0:
366
+ return 0.0
367
+ return dot_product / (norm1 * norm2)
368
+
321
369
  def _errors_similar(self, e1: str, e2: str) -> bool:
322
370
  """Check if two errors are similar."""
323
371
  # Simple substring check
@@ -5,9 +5,9 @@ Enforces scope constraints and validates learning requests.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Optional, List, Dict, Any, Set
9
8
  from dataclasses import dataclass, field
10
9
  from enum import Enum
10
+ from typing import Any, Dict, List, Optional, Set
11
11
 
12
12
  from alma.types import MemoryScope
13
13
 
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
16
16
 
17
17
  class ValidationResult(Enum):
18
18
  """Result of a validation check."""
19
+
19
20
  ALLOWED = "allowed"
20
21
  DENIED_OUT_OF_SCOPE = "denied_out_of_scope"
21
22
  DENIED_FORBIDDEN = "denied_forbidden"
@@ -26,6 +27,7 @@ class ValidationResult(Enum):
26
27
  @dataclass
27
28
  class ValidationReport:
28
29
  """Detailed report of a validation check."""
30
+
29
31
  result: ValidationResult
30
32
  agent: str
31
33
  domain: str
@@ -36,7 +38,10 @@ class ValidationReport:
36
38
  @property
37
39
  def is_allowed(self) -> bool:
38
40
  """Check if the validation passed."""
39
- return self.result in (ValidationResult.ALLOWED, ValidationResult.WARNING_NO_SCOPE)
41
+ return self.result in (
42
+ ValidationResult.ALLOWED,
43
+ ValidationResult.WARNING_NO_SCOPE,
44
+ )
40
45
 
41
46
  def to_dict(self) -> Dict[str, Any]:
42
47
  """Convert to dictionary."""
alma/mcp/__init__.py CHANGED
@@ -24,13 +24,13 @@ Integration with Claude Code (.mcp.json):
24
24
 
25
25
  from alma.mcp.server import ALMAMCPServer
26
26
  from alma.mcp.tools import (
27
- alma_retrieve,
28
- alma_learn,
29
- alma_add_preference,
30
27
  alma_add_knowledge,
28
+ alma_add_preference,
31
29
  alma_forget,
32
- alma_stats,
33
30
  alma_health,
31
+ alma_learn,
32
+ alma_retrieve,
33
+ alma_stats,
34
34
  )
35
35
 
36
36
  __all__ = [
alma/mcp/__main__.py CHANGED
@@ -78,7 +78,8 @@ Examples:
78
78
  )
79
79
 
80
80
  parser.add_argument(
81
- "--verbose", "-v",
81
+ "--verbose",
82
+ "-v",
82
83
  action="store_true",
83
84
  help="Enable verbose logging",
84
85
  )
alma/mcp/resources.py CHANGED
@@ -6,10 +6,9 @@ Resources represent configuration and metadata about the ALMA instance.
6
6
  """
7
7
 
8
8
  import logging
9
- from typing import Dict, Any, List
9
+ from typing import Any, Dict, List
10
10
 
11
11
  from alma import ALMA
12
- from alma.types import MemoryScope
13
12
 
14
13
  logger = logging.getLogger(__name__)
15
14
 
@@ -70,20 +69,22 @@ def get_agents_resource(alma: ALMA) -> Dict[str, Any]:
70
69
  except Exception:
71
70
  stats = {}
72
71
 
73
- agents.append({
74
- "name": name,
75
- "scope": {
76
- "can_learn": scope.can_learn,
77
- "cannot_learn": scope.cannot_learn,
78
- "min_occurrences_for_heuristic": scope.min_occurrences_for_heuristic,
79
- },
80
- "stats": {
81
- "heuristics_count": stats.get("heuristics_count", 0),
82
- "outcomes_count": stats.get("outcomes_count", 0),
83
- "domain_knowledge_count": stats.get("domain_knowledge_count", 0),
84
- "anti_patterns_count": stats.get("anti_patterns_count", 0),
85
- },
86
- })
72
+ agents.append(
73
+ {
74
+ "name": name,
75
+ "scope": {
76
+ "can_learn": scope.can_learn,
77
+ "cannot_learn": scope.cannot_learn,
78
+ "min_occurrences_for_heuristic": scope.min_occurrences_for_heuristic,
79
+ },
80
+ "stats": {
81
+ "heuristics_count": stats.get("heuristics_count", 0),
82
+ "outcomes_count": stats.get("outcomes_count", 0),
83
+ "domain_knowledge_count": stats.get("domain_knowledge_count", 0),
84
+ "anti_patterns_count": stats.get("anti_patterns_count", 0),
85
+ },
86
+ }
87
+ )
87
88
 
88
89
  return {
89
90
  "uri": "alma://agents",
alma/mcp/server.py CHANGED
@@ -9,23 +9,23 @@ import asyncio
9
9
  import json
10
10
  import logging
11
11
  import sys
12
- from typing import Dict, Any, Optional, List, Callable
13
- from datetime import datetime, timezone
12
+ from typing import Any, Dict, List, Optional
14
13
 
15
14
  from alma import ALMA
15
+ from alma.mcp.resources import (
16
+ get_agents_resource,
17
+ get_config_resource,
18
+ list_resources,
19
+ )
16
20
  from alma.mcp.tools import (
17
- alma_retrieve,
18
- alma_learn,
19
- alma_add_preference,
20
21
  alma_add_knowledge,
22
+ alma_add_preference,
23
+ alma_consolidate,
21
24
  alma_forget,
22
- alma_stats,
23
25
  alma_health,
24
- )
25
- from alma.mcp.resources import (
26
- get_config_resource,
27
- get_agents_resource,
28
- list_resources,
26
+ alma_learn,
27
+ alma_retrieve,
28
+ alma_stats,
29
29
  )
30
30
 
31
31
  logger = logging.getLogger(__name__)
@@ -235,6 +235,41 @@ class ALMAMCPServer:
235
235
  "properties": {},
236
236
  },
237
237
  },
238
+ {
239
+ "name": "alma_consolidate",
240
+ "description": "Consolidate similar memories to reduce redundancy. Merges near-duplicate memories based on semantic similarity. Use dry_run=true first to preview what would be merged.",
241
+ "inputSchema": {
242
+ "type": "object",
243
+ "properties": {
244
+ "agent": {
245
+ "type": "string",
246
+ "description": "Agent whose memories to consolidate",
247
+ },
248
+ "memory_type": {
249
+ "type": "string",
250
+ "enum": [
251
+ "heuristics",
252
+ "outcomes",
253
+ "domain_knowledge",
254
+ "anti_patterns",
255
+ ],
256
+ "description": "Type of memory to consolidate (default: heuristics)",
257
+ "default": "heuristics",
258
+ },
259
+ "similarity_threshold": {
260
+ "type": "number",
261
+ "description": "Minimum cosine similarity to group memories (0.0-1.0, default: 0.85). Higher values are more conservative.",
262
+ "default": 0.85,
263
+ },
264
+ "dry_run": {
265
+ "type": "boolean",
266
+ "description": "If true, preview what would be merged without modifying storage (default: true)",
267
+ "default": True,
268
+ },
269
+ },
270
+ "required": ["agent"],
271
+ },
272
+ },
238
273
  ]
239
274
 
240
275
  async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
@@ -281,17 +316,20 @@ class ALMAMCPServer:
281
316
  params: Dict[str, Any],
282
317
  ) -> Dict[str, Any]:
283
318
  """Handle initialize request."""
284
- return self._success_response(request_id, {
285
- "protocolVersion": "2024-11-05",
286
- "serverInfo": {
287
- "name": self.server_name,
288
- "version": self.server_version,
289
- },
290
- "capabilities": {
291
- "tools": {},
292
- "resources": {},
319
+ return self._success_response(
320
+ request_id,
321
+ {
322
+ "protocolVersion": "2024-11-05",
323
+ "serverInfo": {
324
+ "name": self.server_name,
325
+ "version": self.server_version,
326
+ },
327
+ "capabilities": {
328
+ "tools": {},
329
+ "resources": {},
330
+ },
293
331
  },
294
- })
332
+ )
295
333
 
296
334
  def _handle_tools_list(self, request_id: Optional[int]) -> Dict[str, Any]:
297
335
  """Handle tools/list request."""
@@ -351,6 +389,13 @@ class ALMAMCPServer:
351
389
  agent=arguments.get("agent"),
352
390
  ),
353
391
  "alma_health": lambda: alma_health(self.alma),
392
+ "alma_consolidate": lambda: alma_consolidate(
393
+ self.alma,
394
+ agent=arguments.get("agent", ""),
395
+ memory_type=arguments.get("memory_type", "heuristics"),
396
+ similarity_threshold=arguments.get("similarity_threshold", 0.85),
397
+ dry_run=arguments.get("dry_run", True),
398
+ ),
354
399
  }
355
400
 
356
401
  if tool_name not in tool_handlers:
@@ -362,14 +407,21 @@ class ALMAMCPServer:
362
407
 
363
408
  result = tool_handlers[tool_name]()
364
409
 
365
- return self._success_response(request_id, {
366
- "content": [
367
- {
368
- "type": "text",
369
- "text": json.dumps(result, indent=2),
370
- }
371
- ],
372
- })
410
+ # Handle async functions (like alma_consolidate)
411
+ if asyncio.iscoroutine(result):
412
+ result = await result
413
+
414
+ return self._success_response(
415
+ request_id,
416
+ {
417
+ "content": [
418
+ {
419
+ "type": "text",
420
+ "text": json.dumps(result, indent=2),
421
+ }
422
+ ],
423
+ },
424
+ )
373
425
 
374
426
  def _handle_resources_list(
375
427
  self,
@@ -397,15 +449,18 @@ class ALMAMCPServer:
397
449
  f"Unknown resource: {uri}",
398
450
  )
399
451
 
400
- return self._success_response(request_id, {
401
- "contents": [
402
- {
403
- "uri": resource["uri"],
404
- "mimeType": resource["mimeType"],
405
- "text": json.dumps(resource["content"], indent=2),
406
- }
407
- ],
408
- })
452
+ return self._success_response(
453
+ request_id,
454
+ {
455
+ "contents": [
456
+ {
457
+ "uri": resource["uri"],
458
+ "mimeType": resource["mimeType"],
459
+ "text": json.dumps(resource["content"], indent=2),
460
+ }
461
+ ],
462
+ },
463
+ )
409
464
 
410
465
  def _success_response(
411
466
  self,
@@ -437,15 +492,16 @@ class ALMAMCPServer:
437
492
 
438
493
  async def run_stdio(self):
439
494
  """Run the server in stdio mode for Claude Code integration."""
440
- logger.info(f"Starting ALMA MCP Server (stdio mode)")
495
+ logger.info("Starting ALMA MCP Server (stdio mode)")
441
496
 
442
497
  reader = asyncio.StreamReader()
443
498
  protocol = asyncio.StreamReaderProtocol(reader)
444
- await asyncio.get_event_loop().connect_read_pipe(
445
- lambda: protocol, sys.stdin
446
- )
499
+ await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
447
500
 
448
- writer_transport, writer_protocol = await asyncio.get_event_loop().connect_write_pipe(
501
+ (
502
+ writer_transport,
503
+ writer_protocol,
504
+ ) = await asyncio.get_event_loop().connect_write_pipe(
449
505
  asyncio.streams.FlowControlMixin, sys.stdout
450
506
  )
451
507
  writer = asyncio.StreamWriter(
@@ -497,7 +553,9 @@ class ALMAMCPServer:
497
553
  try:
498
554
  from aiohttp import web
499
555
  except ImportError:
500
- logger.error("aiohttp required for HTTP mode. Install with: pip install aiohttp")
556
+ logger.error(
557
+ "aiohttp required for HTTP mode. Install with: pip install aiohttp"
558
+ )
501
559
  return
502
560
 
503
561
  async def handle_post(request: web.Request) -> web.Response: