ai-microcore 4.0.0.dev16__tar.gz → 4.0.0.dev18__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 (43) hide show
  1. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/PKG-INFO +1 -1
  2. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/__init__.py +3 -1
  3. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/mcp.py +127 -12
  4. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/LICENSE +0 -0
  5. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/README.md +0 -0
  6. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/_env.py +0 -0
  7. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/_llm_functions.py +0 -0
  8. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/_prepare_llm_args.py +0 -0
  9. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/ai_func/__init__.py +0 -0
  10. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/ai_func/ai-func.json.j2 +0 -0
  11. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/ai_func/ai-func.pythonic.j2 +0 -0
  12. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/ai_modules.py +0 -0
  13. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/configuration.py +0 -0
  14. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/embedding_db/__init__.py +0 -0
  15. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/embedding_db/chromadb.py +0 -0
  16. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/file_storage.py +0 -0
  17. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/interactive_setup.py +0 -0
  18. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/json_parsing.py +0 -0
  19. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/__init__.py +0 -0
  20. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/_openai_llm_v0.py +0 -0
  21. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/_openai_llm_v1.py +0 -0
  22. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/anthropic.py +0 -0
  23. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/google_genai.py +0 -0
  24. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/google_vertex_ai.py +0 -0
  25. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/local_llm.py +0 -0
  26. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/local_transformers.py +0 -0
  27. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/openai_llm.py +0 -0
  28. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/llm/shared.py +0 -0
  29. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/logging.py +0 -0
  30. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/message_types.py +0 -0
  31. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/metrics.py +0 -0
  32. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/python.py +0 -0
  33. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/templating/__init__.py +0 -0
  34. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/templating/jinja2.py +0 -0
  35. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/text2speech/elevenlabs.py +0 -0
  36. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/tokenizing.py +0 -0
  37. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/types.py +0 -0
  38. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/ui.py +0 -0
  39. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/utils.py +0 -0
  40. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/wrappers/__init__.py +0 -0
  41. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/wrappers/llm_response_wrapper.py +0 -0
  42. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/microcore/wrappers/prompt_wrapper.py +0 -0
  43. {ai_microcore-4.0.0.dev16 → ai_microcore-4.0.0.dev18}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-microcore
3
- Version: 4.0.0.dev16
3
+ Version: 4.0.0.dev18
4
4
  Summary: # Minimalistic Foundation for AI Applications
5
5
  Keywords: llm,large language models,ai,similarity search,ai search,gpt,openai,framework,adapter
6
6
  Author-email: Vitalii Stepanenko <mail@vitalii.in>
@@ -23,6 +23,7 @@ from .wrappers.llm_response_wrapper import LLMResponse
23
23
  from ._llm_functions import llm, allm, llm_parallel
24
24
  from .utils import parse, dedent
25
25
  from .metrics import Metrics
26
+ from .interactive_setup import interactive_setup
26
27
 
27
28
 
28
29
  def tpl(file: os.PathLike[str] | str, **kwargs) -> str | PromptWrapper:
@@ -181,7 +182,8 @@ __all__ = [
181
182
  "mcp_server",
182
183
  "tokenizing",
183
184
  "Metrics",
185
+ "interactive_setup",
184
186
  # "wrappers",
185
187
  ]
186
188
 
187
- __version__ = "4.0.0-dev16"
189
+ __version__ = "4.0.0-dev18"
@@ -2,8 +2,11 @@ import asyncio
2
2
  import logging
3
3
  from typing import Optional
4
4
  from dataclasses import dataclass, field
5
+ from enum import Enum
5
6
 
7
+ import requests
6
8
  from mcp.client.streamable_http import streamablehttp_client
9
+ from mcp.client.sse import sse_client
7
10
  from mcp import ClientSession, types
8
11
 
9
12
  from .utils import ExtendedString, ConvertableToMessage
@@ -37,17 +40,32 @@ class ToolsCache:
37
40
  cached_tools[mcp_url] = tools
38
41
  storage.write_json(ToolsCache.FILE, cached_tools)
39
42
 
43
+ @staticmethod
44
+ def clear():
45
+ logging.info("Clearing MCP tools cache...")
46
+ storage.delete(ToolsCache.FILE)
47
+
40
48
 
41
49
  class MCPAnswer(ExtendedString, ConvertableToMessage):
42
50
  ...
43
51
 
44
52
 
53
+ class McpTransport(str, Enum):
54
+ SSE: str = "sse"
55
+ STREAMABLE_HTTP: str = "streamable_http"
56
+ WS: str = "ws"
57
+ STDIO: str = "stdio"
58
+
59
+ def __str__(self):
60
+ return self.value
61
+
62
+
45
63
  @dataclass
46
64
  class MCPConnection:
47
65
  url: str = None
66
+ transport: McpTransport = field(default=McpTransport.STREAMABLE_HTTP)
48
67
  read_stream: any = None
49
68
  write_stream: any = None
50
- connection: any = None
51
69
  context_manager: any = None
52
70
  session: ClientSession = field(default=None, init=False)
53
71
  tools: Optional["Tools"] = field(default=None, init=False)
@@ -57,8 +75,10 @@ class MCPConnection:
57
75
  @staticmethod
58
76
  async def init(
59
77
  url: str,
78
+ transport: McpTransport,
60
79
  fetch_tools: bool = True,
61
80
  use_cache: bool = True,
81
+ connect_timeout: float = 10,
62
82
  ) -> "MCPConnection":
63
83
 
64
84
  del_event = asyncio.Event()
@@ -69,17 +89,26 @@ class MCPConnection:
69
89
  # That's a bit of a hack for closing the async context managers
70
90
  async def lifecycle():
71
91
  try:
72
- logging.info(f"Connecting to MCP {url}...")
73
- context_manager = streamablehttp_client(url)
74
- (
75
- read_stream,
76
- write_stream,
77
- connection
78
- ) = await context_manager.__aenter__() # pylint: disable=E1101
92
+ logging.info(f"Connecting to {transport} MCP {url}...")
93
+ if transport == McpTransport.STREAMABLE_HTTP:
94
+ context_manager = streamablehttp_client(url)
95
+ (
96
+ read_stream,
97
+ write_stream,
98
+ _
99
+ ) = await context_manager.__aenter__() # pylint: disable=E1101
100
+ elif transport == McpTransport.SSE:
101
+ context_manager = sse_client(url)
102
+ (
103
+ read_stream,
104
+ write_stream
105
+ ) = await context_manager.__aenter__() # pylint: disable=E1101
106
+ else:
107
+ raise ValueError(f"Unsupported transport type: {transport}")
79
108
  con.url = url
109
+ con.transport = transport
80
110
  con.read_stream = read_stream
81
111
  con.write_stream = write_stream
82
- con.connection = connection
83
112
  con.context_manager = context_manager
84
113
  await con.init_session()
85
114
  if fetch_tools:
@@ -91,7 +120,15 @@ class MCPConnection:
91
120
  await con._close() # pylint: disable=W0212
92
121
 
93
122
  con._lifecycle_task = asyncio.create_task(lifecycle()) # pylint: disable=W0212
94
- await opened_event.wait()
123
+ try:
124
+ await asyncio.wait_for(opened_event.wait(), timeout=connect_timeout)
125
+ except Exception as e:
126
+ logging.warning(f"Failed to connect to MCP {url}: {e}")
127
+ try:
128
+ await con._close() # pylint: disable=W0212
129
+ except: # noqa: E722 # pylint: disable=W0702
130
+ pass
131
+ raise
95
132
  return con
96
133
 
97
134
  async def close(self):
@@ -138,12 +175,24 @@ class MCPConnection:
138
175
  mcp_tools = await self.session.list_tools()
139
176
  self.tools = Tools.from_list([Tool.from_mcp(tool) for tool in mcp_tools.tools])
140
177
  if use_cache:
141
- ToolsCache.write(self.url, self.tools)
178
+ self.update_tools_cache()
142
179
  return self.tools
143
180
 
181
+ def update_tools_cache(self):
182
+ if self.tools is None:
183
+ raise RuntimeError("Tools are not fetched yet. Call fetch_tools() first.")
184
+ ToolsCache.write(self.url, self.tools)
185
+
144
186
  def __del__(self):
145
187
  self._del_event.set()
146
188
 
189
+ async def call(self, name: str, **kwargs):
190
+ assert env().config.AI_SYNTAX_FUNCTION_NAME_FIELD not in kwargs
191
+ params = dict(kwargs)
192
+ params[env().config.AI_SYNTAX_FUNCTION_NAME_FIELD] = name
193
+ return await self.exec(params)
194
+
195
+
147
196
  async def exec(self, params: dict | LLMResponse):
148
197
  if isinstance(params, LLMResponse):
149
198
  try:
@@ -247,6 +296,32 @@ class MCPServer:
247
296
  url: str
248
297
  name: str = field(default="")
249
298
  tools: Tools = field(default_factory=Tools)
299
+ transport: McpTransport = field(default=None)
300
+
301
+ @staticmethod
302
+ def _try_sse_or_streamable_http(url) -> tuple[McpTransport, str]:
303
+ if url.endswith("/"):
304
+ test_sse_url = f"{url}sse"
305
+ else:
306
+ test_sse_url = f"{url}/sse"
307
+ try:
308
+ response = requests.request(method="HEAD", url=test_sse_url, timeout=5)
309
+ if response.status_code == 200:
310
+ return McpTransport.SSE, test_sse_url
311
+ except requests.RequestException:
312
+ # not a SSE endpoint or not reachable
313
+ pass
314
+ return McpTransport.STREAMABLE_HTTP, f"{url}/mcp"
315
+
316
+ @staticmethod
317
+ def _guess_transport_type_by_url(url: str) -> Optional[McpTransport]:
318
+ if url.startswith("ws://") or url.startswith("wss://"):
319
+ return McpTransport.WS
320
+ if url.endswith("/mcp"):
321
+ return McpTransport.STREAMABLE_HTTP
322
+ if url.endswith("/sse"):
323
+ return McpTransport.SSE
324
+ return None
250
325
 
251
326
  @staticmethod
252
327
  def name_from_url(url: str) -> str:
@@ -256,13 +331,29 @@ class MCPServer:
256
331
  def __post_init__(self):
257
332
  if not self.name:
258
333
  self.name = MCPServer.name_from_url(self.url)
334
+ if not self.transport:
335
+ self.transport = self._guess_transport_type_by_url(self.url)
259
336
 
260
337
  async def connect(
261
338
  self,
262
339
  fetch_tools: bool = True,
263
340
  use_cache: bool = True,
341
+ connect_timeout: float = 10,
264
342
  ) -> MCPConnection:
265
- return await MCPConnection.init(self.url, fetch_tools=fetch_tools, use_cache=use_cache)
343
+ if self.transport:
344
+ transport, url = self.transport, self.url
345
+ else:
346
+ transport, url = self._try_sse_or_streamable_http(self.url)
347
+ return await MCPConnection.init(
348
+ url=url,
349
+ transport=transport,
350
+ fetch_tools=fetch_tools,
351
+ use_cache=use_cache,
352
+ connect_timeout=connect_timeout,
353
+ )
354
+
355
+ def get_tools_cache(self) -> Tools | None:
356
+ return ToolsCache.read(self.url)
266
357
 
267
358
 
268
359
  class MCPRegistry(dict[str, MCPServer]):
@@ -281,6 +372,30 @@ class MCPRegistry(dict[str, MCPServer]):
281
372
  raise ValueError(f"MCP server '{server_name}' not found in registry")
282
373
  return self[server_name]
283
374
 
375
+ async def precache_tools(
376
+ self,
377
+ raise_errors: bool = False,
378
+ connect_timeout: int = 10,
379
+ ):
380
+ async def precache_server_tools(server_name):
381
+ conn = None
382
+ try:
383
+ conn = await self.get(server_name).connect(
384
+ fetch_tools=True,
385
+ use_cache=False,
386
+ connect_timeout=connect_timeout,
387
+ )
388
+ conn.update_tools_cache()
389
+ except Exception as e: # pylint: disable=W0718
390
+ logging.error("Failed to precache tools for MCP server %s: %s", server_name, e)
391
+ if raise_errors:
392
+ raise
393
+ finally:
394
+ if conn is not None:
395
+ await conn.close()
396
+
397
+ await asyncio.gather(*[precache_server_tools(srv) for srv in self.keys()])
398
+
284
399
  async def connect_to(
285
400
  self,
286
401
  server_name: str,