ai-microcore 4.0.0.dev11__tar.gz → 4.0.0.dev12__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.
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/PKG-INFO +10 -4
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/README.md +5 -2
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/__init__.py +24 -5
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/_env.py +9 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/ai_func/__init__.py +2 -2
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/ai_func/ai-func.json.j2 +1 -1
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/configuration.py +6 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/interactive_setup.py +77 -75
- ai_microcore-4.0.0.dev12/microcore/mcp.py +280 -0
- ai_microcore-4.0.0.dev12/microcore/templating/jinja2.py +28 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/utils.py +14 -1
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/wrappers/llm_response_wrapper.py +11 -2
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/pyproject.toml +4 -1
- ai_microcore-4.0.0.dev11/microcore/templating/jinja2.py +0 -19
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/LICENSE +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/_llm_functions.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/_prepare_llm_args.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/ai_func/ai-func.pythonic.j2 +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/ai_modules.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/embedding_db/__init__.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/embedding_db/chromadb.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/file_storage.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/json_parsing.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/__init__.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/_openai_llm_v0.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/_openai_llm_v1.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/anthropic.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/google_genai.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/google_vertex_ai.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/local_llm.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/local_transformers.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/openai_llm.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/llm/shared.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/logging.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/message_types.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/metrics.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/python.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/templating/__init__.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/text2speech/elevenlabs.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/tokenizing.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/types.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/ui.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/wrappers/__init__.py +0 -0
- {ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/wrappers/prompt_wrapper.py +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-microcore
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev12
|
|
4
4
|
Summary: # Minimalistic Foundation for AI Applications
|
|
5
|
-
Keywords: llm,large language models,ai,similarity search,ai search,gpt,openai
|
|
5
|
+
Keywords: llm,large language models,ai,similarity search,ai search,gpt,openai,framework,adapter
|
|
6
6
|
Author-email: Vitalii Stepanenko <mail@vitalii.in>
|
|
7
7
|
Maintainer-email: Vitalii Stepanenko <mail@vitalii.in>
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
15
|
Classifier: Operating System :: OS Independent
|
|
14
16
|
Classifier: Intended Audience :: Developers
|
|
@@ -21,6 +23,7 @@ Requires-Dist: colorama~=0.4.6
|
|
|
21
23
|
Requires-Dist: PyYAML~=6.0
|
|
22
24
|
Requires-Dist: chardet~=5.2.0
|
|
23
25
|
Requires-Dist: tiktoken>=0.7.0,<1.0
|
|
26
|
+
Requires-Dist: mcp~=1.9.1
|
|
24
27
|
Project-URL: Source Code, https://github.com/Nayjest/ai-microcore
|
|
25
28
|
|
|
26
29
|
<p align="right">
|
|
@@ -35,7 +38,7 @@ Project-URL: Source Code, https://github.com/Nayjest/ai-microcore
|
|
|
35
38
|
# AI MicroCore: A Minimalistic Foundation for AI Applications
|
|
36
39
|
|
|
37
40
|
**MicroCore** is a collection of python adapters for Large Language Models
|
|
38
|
-
and Semantic Search APIs allowing to
|
|
41
|
+
and Vector Databases / Semantic Search APIs allowing to
|
|
39
42
|
communicate with these services in a convenient way, make them easily switchable
|
|
40
43
|
and separate business logic from the implementation details.
|
|
41
44
|
|
|
@@ -45,6 +48,10 @@ without need to change your application code.
|
|
|
45
48
|
|
|
46
49
|
You even can switch between text completion and chat completion models only using configuration.
|
|
47
50
|
|
|
51
|
+
Thanks to LLM-agnostic MCP integration,
|
|
52
|
+
**MicroCore** connects MCP tools to any language models easily,
|
|
53
|
+
whether through API providers that do not support MCP, or through inference using pytorch or arbitrary python functions.
|
|
54
|
+
|
|
48
55
|
The basic example of usage is as follows:
|
|
49
56
|
|
|
50
57
|
```python
|
|
@@ -288,7 +295,6 @@ import microcore.ai_modules
|
|
|
288
295
|
|
|
289
296
|
* Automatically registers template folders of AI modules in Jinja2 environment
|
|
290
297
|
|
|
291
|
-
|
|
292
298
|
## 🛠️ Contributing
|
|
293
299
|
|
|
294
300
|
Please see [CONTRIBUTING](https://github.com/Nayjest/ai-microcore/blob/main/CONTRIBUTING.md) for details.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# AI MicroCore: A Minimalistic Foundation for AI Applications
|
|
11
11
|
|
|
12
12
|
**MicroCore** is a collection of python adapters for Large Language Models
|
|
13
|
-
and Semantic Search APIs allowing to
|
|
13
|
+
and Vector Databases / Semantic Search APIs allowing to
|
|
14
14
|
communicate with these services in a convenient way, make them easily switchable
|
|
15
15
|
and separate business logic from the implementation details.
|
|
16
16
|
|
|
@@ -20,6 +20,10 @@ without need to change your application code.
|
|
|
20
20
|
|
|
21
21
|
You even can switch between text completion and chat completion models only using configuration.
|
|
22
22
|
|
|
23
|
+
Thanks to LLM-agnostic MCP integration,
|
|
24
|
+
**MicroCore** connects MCP tools to any language models easily,
|
|
25
|
+
whether through API providers that do not support MCP, or through inference using pytorch or arbitrary python functions.
|
|
26
|
+
|
|
23
27
|
The basic example of usage is as follows:
|
|
24
28
|
|
|
25
29
|
```python
|
|
@@ -263,7 +267,6 @@ import microcore.ai_modules
|
|
|
263
267
|
|
|
264
268
|
* Automatically registers template folders of AI modules in Jinja2 environment
|
|
265
269
|
|
|
266
|
-
|
|
267
270
|
## 🛠️ Contributing
|
|
268
271
|
|
|
269
272
|
Please see [CONTRIBUTING](https://github.com/Nayjest/ai-microcore/blob/main/CONTRIBUTING.md) for details.
|
|
@@ -8,8 +8,9 @@ and separate business logic from implementation details.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
from . import mcp
|
|
12
|
+
from . import ui
|
|
13
|
+
from . import tokenizing
|
|
13
14
|
from .embedding_db import SearchResult, AbstractEmbeddingDB, SearchResults
|
|
14
15
|
from .file_storage import storage
|
|
15
16
|
from ._env import configure, env, config
|
|
@@ -22,7 +23,6 @@ from .wrappers.llm_response_wrapper import LLMResponse
|
|
|
22
23
|
from ._llm_functions import llm, allm, llm_parallel
|
|
23
24
|
from .utils import parse, dedent
|
|
24
25
|
from .metrics import Metrics
|
|
25
|
-
from .interactive_setup import interactive_setup
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def tpl(file: os.PathLike[str] | str, **kwargs) -> str | PromptWrapper:
|
|
@@ -120,6 +120,23 @@ class _EmbeddingDBProxy(AbstractEmbeddingDB):
|
|
|
120
120
|
texts = _EmbeddingDBProxy()
|
|
121
121
|
"""Embedding database, see `microcore.embedding_db.AbstractEmbeddingDB`"""
|
|
122
122
|
|
|
123
|
+
|
|
124
|
+
def mcp_server(name: str) -> mcp.MCPServer: # noqa, pylint-disable=E0602
|
|
125
|
+
"""
|
|
126
|
+
Returns MCP server by name from the registry.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name (str): The name of the MCP server.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
MCPServer: The MCP server instance.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If the server with the given name is not found in the registry.
|
|
136
|
+
"""
|
|
137
|
+
return mcp.server(name) # noqa, pylint-disable=E0602
|
|
138
|
+
|
|
139
|
+
|
|
123
140
|
__all__ = [
|
|
124
141
|
"llm",
|
|
125
142
|
"allm",
|
|
@@ -160,9 +177,11 @@ __all__ = [
|
|
|
160
177
|
"Config",
|
|
161
178
|
"types",
|
|
162
179
|
"ui",
|
|
180
|
+
"mcp",
|
|
181
|
+
"mcp_server",
|
|
182
|
+
"tokenizing",
|
|
163
183
|
"Metrics",
|
|
164
|
-
"interactive_setup"
|
|
165
184
|
# "wrappers",
|
|
166
185
|
]
|
|
167
186
|
|
|
168
|
-
__version__ = "4.0.0-
|
|
187
|
+
__version__ = "4.0.0-dev12"
|
|
@@ -15,6 +15,7 @@ from .llm.local_llm import make_llm_functions as make_local_llm_functions
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from .wrappers.llm_response_wrapper import LLMResponse # noqa: F401
|
|
17
17
|
from transformers import PreTrainedModel, PreTrainedTokenizer # noqa: F401
|
|
18
|
+
from .mcp import MCPRegistry
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass
|
|
@@ -33,6 +34,7 @@ class Env:
|
|
|
33
34
|
tokenizer: "PreTrainedTokenizer" = field( # noqa
|
|
34
35
|
default=None, init=False, repr=False
|
|
35
36
|
)
|
|
37
|
+
_mcp_registry: "MCPRegistry" = field(init=False, default=None)
|
|
36
38
|
|
|
37
39
|
def __post_init__(self):
|
|
38
40
|
global _env
|
|
@@ -52,6 +54,13 @@ class Env:
|
|
|
52
54
|
self.jinja_env = make_jinja2_env(self)
|
|
53
55
|
self.tpl_function = make_tpl_function(self)
|
|
54
56
|
|
|
57
|
+
@property
|
|
58
|
+
def mcp_registry(self):
|
|
59
|
+
if self._mcp_registry is None:
|
|
60
|
+
from .mcp import MCPRegistry
|
|
61
|
+
self._mcp_registry = MCPRegistry(self.config.MCP_SERVERS)
|
|
62
|
+
return self._mcp_registry
|
|
63
|
+
|
|
55
64
|
def init_llm(self):
|
|
56
65
|
if self.config.LLM_API_TYPE == ApiType.NONE:
|
|
57
66
|
|
|
@@ -8,8 +8,8 @@ import inspect
|
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from typing import Dict, Any
|
|
10
10
|
import docstring_parser
|
|
11
|
-
from .. import tpl
|
|
12
11
|
from ..utils import dedent
|
|
12
|
+
from .._env import env
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class AiFuncSyntax(str, Enum):
|
|
@@ -95,4 +95,4 @@ def describe_ai_func(func: callable, syntax: AiFuncSyntax | str = None) -> str:
|
|
|
95
95
|
syntax = syntax or AiFuncSyntax.DEFAULT
|
|
96
96
|
tpl_file = f"ai-func.{syntax}.j2" if syntax in AiFuncSyntax else syntax
|
|
97
97
|
metadata = func_metadata(func)
|
|
98
|
-
return
|
|
98
|
+
return env().tpl_function(tpl_file, **metadata)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{ description or name.replace('_', ' ').capitalize() }}
|
|
2
2
|
{
|
|
3
|
-
"
|
|
3
|
+
"{{ config.AI_SYNTAX_FUNCTION_NAME_FIELD }}": "{{ name }}"{% if args %}{{ "," }}{% endif %}
|
|
4
4
|
{%- for k,v in args.items() %}
|
|
5
5
|
"{{ k }}":
|
|
6
6
|
{%- if v.type %} <{{ v.type }}>{% endif -%}
|
|
@@ -404,6 +404,12 @@ class Config(LLMConfig):
|
|
|
404
404
|
- LLMResponse objects will not contain the links to the prompt field
|
|
405
405
|
"""
|
|
406
406
|
|
|
407
|
+
AI_SYNTAX_FUNCTION_NAME_FIELD: str = from_env(default="call")
|
|
408
|
+
|
|
409
|
+
JINJA2_GLOBALS: dict = from_env(dtype=dict)
|
|
410
|
+
|
|
411
|
+
MCP_SERVERS: list = from_env(dtype=list)
|
|
412
|
+
|
|
407
413
|
def __post_init__(self):
|
|
408
414
|
super().__post_init__()
|
|
409
415
|
if self.TEXT_TO_SPEECH_PATH is None:
|
|
@@ -1,75 +1,77 @@
|
|
|
1
|
-
from .configuration import EmbeddingDbType, ApiType
|
|
2
|
-
from .ui import ask_choose, ask_non_empty, ask_yn, error, yellow
|
|
3
|
-
from ._env import configure
|
|
4
|
-
from ._llm_functions import llm
|
|
5
|
-
from .utils import file_link
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def interactive_setup(
|
|
9
|
-
file_path: str,
|
|
10
|
-
defaults: dict = None,
|
|
11
|
-
extras: dict | list = None,
|
|
12
|
-
):
|
|
13
|
-
"""
|
|
14
|
-
Interactive setup for LLM API configuration.
|
|
15
|
-
Prompts user for configuration details such as API type, key, model name,
|
|
16
|
-
and base URL. Tests the LLM API with a sample query and saves the configuration
|
|
17
|
-
to a specified file if the user chooses to do so.
|
|
18
|
-
Args:
|
|
19
|
-
file_path (str): Path to the configuration file.
|
|
20
|
-
defaults (dict, optional): Default configuration values.
|
|
21
|
-
If provided, user will not be prompted for those values.
|
|
22
|
-
Additional values for storing in the file can be added to defaults.
|
|
23
|
-
extras (dict | list, optional): Additional configuration fields to prompt for.
|
|
24
|
-
"""
|
|
25
|
-
raw_config = dict(defaults) if defaults else dict()
|
|
26
|
-
if "LLM_API_TYPE" not in raw_config:
|
|
27
|
-
raw_config["LLM_API_TYPE"] = ask_choose(
|
|
28
|
-
"Choose LLM API Type:",
|
|
29
|
-
list(i.value for i in ApiType if not ApiType.is_local(i)),
|
|
30
|
-
)
|
|
31
|
-
if "LLM_API_KEY" not in raw_config:
|
|
32
|
-
raw_config["LLM_API_KEY"] = ask_non_empty("API Key: ")
|
|
33
|
-
if "MODEL" not in raw_config:
|
|
34
|
-
raw_config["MODEL"] = ask_non_empty("Model Name: ")
|
|
35
|
-
if "LLM_API_BASE" not in raw_config:
|
|
36
|
-
raw_config["LLM_API_BASE"] = input("API Base URL (may be empty for some API types): ")
|
|
37
|
-
if extras:
|
|
38
|
-
if isinstance(extras, list):
|
|
39
|
-
extras = {
|
|
40
|
-
i:
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.replace('
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
from .configuration import EmbeddingDbType, ApiType
|
|
2
|
+
from .ui import ask_choose, ask_non_empty, ask_yn, error, yellow
|
|
3
|
+
from ._env import configure
|
|
4
|
+
from ._llm_functions import llm
|
|
5
|
+
from .utils import file_link
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def interactive_setup(
|
|
9
|
+
file_path: str,
|
|
10
|
+
defaults: dict = None,
|
|
11
|
+
extras: dict | list = None,
|
|
12
|
+
):
|
|
13
|
+
"""
|
|
14
|
+
Interactive setup for LLM API configuration.
|
|
15
|
+
Prompts user for configuration details such as API type, key, model name,
|
|
16
|
+
and base URL. Tests the LLM API with a sample query and saves the configuration
|
|
17
|
+
to a specified file if the user chooses to do so.
|
|
18
|
+
Args:
|
|
19
|
+
file_path (str): Path to the configuration file.
|
|
20
|
+
defaults (dict, optional): Default configuration values.
|
|
21
|
+
If provided, user will not be prompted for those values.
|
|
22
|
+
Additional values for storing in the file can be added to defaults.
|
|
23
|
+
extras (dict | list, optional): Additional configuration fields to prompt for.
|
|
24
|
+
"""
|
|
25
|
+
raw_config = dict(defaults) if defaults else dict()
|
|
26
|
+
if "LLM_API_TYPE" not in raw_config:
|
|
27
|
+
raw_config["LLM_API_TYPE"] = ask_choose(
|
|
28
|
+
"Choose LLM API Type:",
|
|
29
|
+
list(i.value for i in ApiType if not ApiType.is_local(i)),
|
|
30
|
+
)
|
|
31
|
+
if "LLM_API_KEY" not in raw_config:
|
|
32
|
+
raw_config["LLM_API_KEY"] = ask_non_empty("API Key: ")
|
|
33
|
+
if "MODEL" not in raw_config:
|
|
34
|
+
raw_config["MODEL"] = ask_non_empty("Model Name: ")
|
|
35
|
+
if "LLM_API_BASE" not in raw_config:
|
|
36
|
+
raw_config["LLM_API_BASE"] = input("API Base URL (may be empty for some API types): ")
|
|
37
|
+
if extras:
|
|
38
|
+
if isinstance(extras, list):
|
|
39
|
+
extras = {
|
|
40
|
+
i: (
|
|
41
|
+
str(i)
|
|
42
|
+
.replace('_', ' ')
|
|
43
|
+
.capitalize()
|
|
44
|
+
.replace('Llm', 'LLM')
|
|
45
|
+
.replace('Api', 'API')
|
|
46
|
+
)
|
|
47
|
+
for i in extras
|
|
48
|
+
}
|
|
49
|
+
for field, title in extras.items():
|
|
50
|
+
if field not in raw_config:
|
|
51
|
+
raw_config[field] = ask_non_empty(f"{title}: ")
|
|
52
|
+
try:
|
|
53
|
+
configure(
|
|
54
|
+
**{
|
|
55
|
+
**dict(
|
|
56
|
+
USE_DOT_ENV=False,
|
|
57
|
+
EMBEDDING_DB_TYPE=EmbeddingDbType.NONE,
|
|
58
|
+
USE_LOGGING=True,
|
|
59
|
+
),
|
|
60
|
+
**raw_config
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
print("Testing LLM...")
|
|
64
|
+
q = "What is capital of France?\n(!) IMPORTANT: Answer only with one word"
|
|
65
|
+
assert "pari" in llm(q).lower()
|
|
66
|
+
except Exception as e: # pylint: disable=W0718
|
|
67
|
+
error(f"Error testing LLM API: {e}")
|
|
68
|
+
if ask_yn("Restart configuring?"):
|
|
69
|
+
interactive_setup(file_path, defaults, extras)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
config_body = ''.join(f"{k}={v}\n" for k, v in raw_config.items())
|
|
73
|
+
print(f"Configuration:\n{yellow(config_body)}")
|
|
74
|
+
if ask_yn("Save configuration to file?"):
|
|
75
|
+
print(f"Saved to {file_link(file_path)}")
|
|
76
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
77
|
+
f.write(config_body)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
7
|
+
from mcp import ClientSession, types
|
|
8
|
+
|
|
9
|
+
from .utils import ExtendedString
|
|
10
|
+
from .ai_func import AiFuncSyntax
|
|
11
|
+
from . import ui
|
|
12
|
+
from .types import BadAIAnswer, BadAIJsonAnswer
|
|
13
|
+
from .wrappers.llm_response_wrapper import LLMResponse
|
|
14
|
+
from ._env import env
|
|
15
|
+
from .file_storage import storage
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WrongMcpUsage(BadAIAnswer):
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolsCache:
|
|
23
|
+
FILE = "mcp/tools.json"
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def read(mcp_url: str) -> Optional["Tools"]:
|
|
27
|
+
logging.info(f"Checking MCP tools cache for {ui.green(mcp_url)}...")
|
|
28
|
+
tools = storage.read_json(ToolsCache.FILE, default={}).get(mcp_url, None)
|
|
29
|
+
if tools is not None:
|
|
30
|
+
return Tools.from_list([Tool(**raw) for raw in tools.values()])
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def write(mcp_url: str, tools: "Tools"):
|
|
35
|
+
logging.info(f"Storing MCP tools cache for {ui.green(mcp_url)}...")
|
|
36
|
+
cached_tools = storage.read_json(ToolsCache.FILE, default={})
|
|
37
|
+
cached_tools[mcp_url] = tools
|
|
38
|
+
storage.write_json(ToolsCache.FILE, cached_tools)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class MCPConnection:
|
|
43
|
+
url: str = None
|
|
44
|
+
read_stream: any = None
|
|
45
|
+
write_stream: any = None
|
|
46
|
+
connection: any = None
|
|
47
|
+
context_manager: any = None
|
|
48
|
+
session: ClientSession = field(default=None, init=False)
|
|
49
|
+
del_event: asyncio.Event = field(default=None, init=False)
|
|
50
|
+
tools: Optional["Tools"] = field(default=None, init=False)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
async def init(
|
|
54
|
+
url: str,
|
|
55
|
+
fetch_tools: bool = True,
|
|
56
|
+
use_cache: bool = True,
|
|
57
|
+
) -> "MCPConnection":
|
|
58
|
+
|
|
59
|
+
del_event = asyncio.Event()
|
|
60
|
+
opened_event = asyncio.Event()
|
|
61
|
+
con: MCPConnection = MCPConnection()
|
|
62
|
+
con.del_event = del_event
|
|
63
|
+
|
|
64
|
+
# That's a bit of a hack for closing the async context managers
|
|
65
|
+
async def lifecycle():
|
|
66
|
+
try:
|
|
67
|
+
logging.info(f"Connecting to MCP {url}...")
|
|
68
|
+
context_manager = streamablehttp_client(url)
|
|
69
|
+
(
|
|
70
|
+
read_stream,
|
|
71
|
+
write_stream,
|
|
72
|
+
connection
|
|
73
|
+
) = await context_manager.__aenter__() # pylint: disable=E1101
|
|
74
|
+
con.url = url
|
|
75
|
+
con.read_stream = read_stream
|
|
76
|
+
con.write_stream = write_stream
|
|
77
|
+
con.connection = connection
|
|
78
|
+
con.context_manager = context_manager
|
|
79
|
+
await con.init_session()
|
|
80
|
+
if fetch_tools:
|
|
81
|
+
await con.fetch_tools(use_cache=use_cache)
|
|
82
|
+
opened_event.set()
|
|
83
|
+
await del_event.wait()
|
|
84
|
+
finally:
|
|
85
|
+
await con.close()
|
|
86
|
+
|
|
87
|
+
asyncio.create_task(lifecycle())
|
|
88
|
+
await opened_event.wait()
|
|
89
|
+
return con
|
|
90
|
+
|
|
91
|
+
async def close(self):
|
|
92
|
+
logging.info(f"Closing MCP session ({self.url})...")
|
|
93
|
+
try:
|
|
94
|
+
if self.session:
|
|
95
|
+
await self.session.__aexit__(None, None, None)
|
|
96
|
+
del self.session
|
|
97
|
+
self.session = None
|
|
98
|
+
finally:
|
|
99
|
+
logging.info(f"Closing MCP connection ({self.url})...")
|
|
100
|
+
try:
|
|
101
|
+
if self.context_manager:
|
|
102
|
+
await self.context_manager.__aexit__(None, None, None)
|
|
103
|
+
del self.context_manager
|
|
104
|
+
self.context_manager = None
|
|
105
|
+
finally:
|
|
106
|
+
logging.info(f"Closed CTX ({self.url})")
|
|
107
|
+
|
|
108
|
+
async def init_session(self):
|
|
109
|
+
logging.info(f"Initializing MCP session ({self.url})")
|
|
110
|
+
self.session = ClientSession(self.read_stream, self.write_stream)
|
|
111
|
+
await self.session.__aenter__() # pylint: disable=unnecessary-dunder-call
|
|
112
|
+
await self.session.initialize()
|
|
113
|
+
return self.session
|
|
114
|
+
|
|
115
|
+
async def fetch_tools(self, use_cache: bool = True) -> "Tools":
|
|
116
|
+
if self.tools is not None:
|
|
117
|
+
return self.tools
|
|
118
|
+
|
|
119
|
+
if use_cache and (cached_tools := ToolsCache.read(self.url)):
|
|
120
|
+
logging.info("Using MCP tools from cache for %s", ui.green(self.url))
|
|
121
|
+
self.tools = cached_tools
|
|
122
|
+
return self.tools
|
|
123
|
+
|
|
124
|
+
logging.info("Fetching tools from MCP %s", ui.green(self.url))
|
|
125
|
+
mcp_tools = await self.session.list_tools()
|
|
126
|
+
self.tools = Tools.from_list([Tool.from_mcp(tool) for tool in mcp_tools.tools])
|
|
127
|
+
if use_cache:
|
|
128
|
+
ToolsCache.write(self.url, self.tools)
|
|
129
|
+
return self.tools
|
|
130
|
+
|
|
131
|
+
def __del__(self):
|
|
132
|
+
self.del_event.set()
|
|
133
|
+
|
|
134
|
+
async def exec(self, params: dict | LLMResponse):
|
|
135
|
+
if isinstance(params, LLMResponse):
|
|
136
|
+
try:
|
|
137
|
+
params = params.parse_json(
|
|
138
|
+
raise_errors=True,
|
|
139
|
+
required_fields=[env().config.AI_SYNTAX_FUNCTION_NAME_FIELD],
|
|
140
|
+
)
|
|
141
|
+
except BadAIJsonAnswer as e:
|
|
142
|
+
raise WrongMcpUsage(str(e)) from e
|
|
143
|
+
params = dict(params)
|
|
144
|
+
name = params.pop(env().config.AI_SYNTAX_FUNCTION_NAME_FIELD)
|
|
145
|
+
if not name:
|
|
146
|
+
raise WrongMcpUsage(
|
|
147
|
+
f"Tool name should be passed in {env().config.AI_SYNTAX_FUNCTION_NAME_FIELD} field"
|
|
148
|
+
)
|
|
149
|
+
logging.info(f"Calling MCP tool {ui.green(name)} with {params}...")
|
|
150
|
+
result = await self.session.call_tool(name, params)
|
|
151
|
+
content = result.content
|
|
152
|
+
if content and len(content) == 1 and content[0].type == "text":
|
|
153
|
+
return ExtendedString(content[0].text, result.__dict__)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class Tool:
|
|
159
|
+
@dataclass
|
|
160
|
+
class Arg:
|
|
161
|
+
name: str = field()
|
|
162
|
+
description: str = field(default="")
|
|
163
|
+
type: str = field(default="string")
|
|
164
|
+
required: bool = field(default=...)
|
|
165
|
+
default: any = field(default=...)
|
|
166
|
+
|
|
167
|
+
def __post_init__(self):
|
|
168
|
+
if self.required is ...:
|
|
169
|
+
self.required = self.default is ...
|
|
170
|
+
if self.default is ...:
|
|
171
|
+
self.default = None
|
|
172
|
+
|
|
173
|
+
name: str = field()
|
|
174
|
+
description: str = field(default="")
|
|
175
|
+
args: dict[str, Arg | dict] = field(default_factory=dict)
|
|
176
|
+
|
|
177
|
+
def __post_init__(self):
|
|
178
|
+
for key in self.args.keys(): # pylint: disable=C0201, C0206
|
|
179
|
+
if isinstance(self.args[key], dict):
|
|
180
|
+
self.args[key] = Tool.Arg(**self.args[key])
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def from_mcp(tool: types.Tool) -> "Tool":
|
|
184
|
+
t = Tool(
|
|
185
|
+
name=tool.name,
|
|
186
|
+
description=tool.description,
|
|
187
|
+
)
|
|
188
|
+
for param_name, data in tool.inputSchema.get("properties", {}).items():
|
|
189
|
+
param = Tool.Arg(
|
|
190
|
+
name=param_name,
|
|
191
|
+
description=data.get("title", ""),
|
|
192
|
+
type=data.get("type", "string"),
|
|
193
|
+
required=param_name in tool.inputSchema.get("required", []),
|
|
194
|
+
default="",
|
|
195
|
+
)
|
|
196
|
+
t.args[param_name] = param
|
|
197
|
+
return t
|
|
198
|
+
|
|
199
|
+
def describe(self, syntax: AiFuncSyntax = None) -> str:
|
|
200
|
+
syntax = syntax or AiFuncSyntax.DEFAULT
|
|
201
|
+
tpl_file = f"ai-func.{syntax}.j2" if syntax in AiFuncSyntax else syntax
|
|
202
|
+
metadata = self._get_metadata()
|
|
203
|
+
return env().tpl_function(tpl_file, **metadata)
|
|
204
|
+
|
|
205
|
+
def _get_metadata(self):
|
|
206
|
+
return dict(
|
|
207
|
+
name=self.name,
|
|
208
|
+
description=self.description,
|
|
209
|
+
args={
|
|
210
|
+
arg.name: dict(
|
|
211
|
+
default="NOT_SET" if arg.required else arg.default,
|
|
212
|
+
type=arg.type,
|
|
213
|
+
comment=arg.description, # @todo rename it to description
|
|
214
|
+
)
|
|
215
|
+
for arg in self.args.values()
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def __str__(self):
|
|
220
|
+
return self.describe(syntax=AiFuncSyntax.DEFAULT)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class Tools(dict[str, Tool]):
|
|
224
|
+
def __str__(self):
|
|
225
|
+
return "\n".join([str(tool) for tool in self.values()])
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def from_list(tools: list[Tool]) -> "Tools":
|
|
229
|
+
return Tools({tool.name: tool for tool in tools})
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class MCPServer:
|
|
234
|
+
name: str
|
|
235
|
+
url: str
|
|
236
|
+
tools: Tools = field(default_factory=Tools)
|
|
237
|
+
|
|
238
|
+
async def connect(
|
|
239
|
+
self,
|
|
240
|
+
fetch_tools: bool = True,
|
|
241
|
+
use_cache: bool = True,
|
|
242
|
+
) -> MCPConnection:
|
|
243
|
+
return await MCPConnection.init(self.url, fetch_tools=fetch_tools, use_cache=use_cache)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class MCPRegistry(dict[str, MCPServer]):
|
|
247
|
+
def __init__(self, server_configs: list[dict]):
|
|
248
|
+
super().__init__()
|
|
249
|
+
for server_config in server_configs:
|
|
250
|
+
self[server_config["name"]] = MCPServer(**server_config)
|
|
251
|
+
|
|
252
|
+
def get(self, server_name: str) -> MCPServer:
|
|
253
|
+
if server_name not in self:
|
|
254
|
+
raise ValueError(f"MCP server '{server_name}' not found in registry")
|
|
255
|
+
return self[server_name]
|
|
256
|
+
|
|
257
|
+
async def connect_to(
|
|
258
|
+
self,
|
|
259
|
+
server_name: str,
|
|
260
|
+
fetch_tools: bool = True,
|
|
261
|
+
use_cache: bool = True,
|
|
262
|
+
) -> MCPConnection:
|
|
263
|
+
mcp_server = self.get(server_name)
|
|
264
|
+
return await mcp_server.connect(fetch_tools=fetch_tools, use_cache=use_cache)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def server(name: str) -> MCPServer:
|
|
268
|
+
"""
|
|
269
|
+
Returns MCP server by name from the registry.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
name (str): The name of the MCP server.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
MCPServer: The MCP server instance.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ValueError: If the server with the given name is not found in the registry.
|
|
279
|
+
"""
|
|
280
|
+
return env().mcp_registry.get(name)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import jinja2
|
|
5
|
+
from ..types import TplFunctionType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_jinja2_env(env) -> jinja2.Environment:
|
|
9
|
+
j2 = jinja2.Environment(
|
|
10
|
+
autoescape=env.config.JINJA2_AUTO_ESCAPE,
|
|
11
|
+
loader=jinja2.ChoiceLoader([
|
|
12
|
+
jinja2.FileSystemLoader(env.config.PROMPT_TEMPLATES_PATH),
|
|
13
|
+
jinja2.FileSystemLoader(Path(__file__).parent.parent / "ai_func"),
|
|
14
|
+
]),
|
|
15
|
+
)
|
|
16
|
+
j2.globals.update(
|
|
17
|
+
env=env,
|
|
18
|
+
config=env.config,
|
|
19
|
+
**env.config.JINJA2_GLOBALS,
|
|
20
|
+
)
|
|
21
|
+
return j2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_tpl_function(env) -> TplFunctionType:
|
|
25
|
+
def tpl(file: os.PathLike[str] | str, **kwargs) -> str:
|
|
26
|
+
return env.jinja_env.get_template(file).render(**kwargs)
|
|
27
|
+
|
|
28
|
+
return tpl
|
|
@@ -16,8 +16,9 @@ import tiktoken
|
|
|
16
16
|
from colorama import Fore
|
|
17
17
|
|
|
18
18
|
from .configuration import Config
|
|
19
|
-
from .types import BadAIAnswer
|
|
19
|
+
from .types import BadAIAnswer, BadAIJsonAnswer
|
|
20
20
|
from .message_types import UserMsg, SysMsg, AssistantMsg
|
|
21
|
+
from .json_parsing import parse_json
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def is_chat_model(model: str, config: Config = None) -> bool:
|
|
@@ -101,6 +102,18 @@ class ExtendedString(str):
|
|
|
101
102
|
"""
|
|
102
103
|
return len(self.to_tokens(for_model=for_model, encoding=encoding))
|
|
103
104
|
|
|
105
|
+
def parse_json(
|
|
106
|
+
self, raise_errors: bool = True, required_fields: list[str] = None
|
|
107
|
+
) -> list | dict | float | int | str:
|
|
108
|
+
return parse_json(self, raise_errors, required_fields)
|
|
109
|
+
|
|
110
|
+
def contains_valid_json(self) -> bool:
|
|
111
|
+
try:
|
|
112
|
+
self.parse_json(raise_errors=True)
|
|
113
|
+
return True
|
|
114
|
+
except BadAIJsonAnswer:
|
|
115
|
+
return False
|
|
116
|
+
|
|
104
117
|
|
|
105
118
|
class DataclassEncoder(json.JSONEncoder):
|
|
106
119
|
"""@private"""
|
{ai_microcore-4.0.0.dev11 → ai_microcore-4.0.0.dev12}/microcore/wrappers/llm_response_wrapper.py
RENAMED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
2
3
|
|
|
3
4
|
from ..types import BadAIAnswer, TPrompt
|
|
4
|
-
from ..json_parsing import parse_json
|
|
5
5
|
from ..utils import ExtendedString, ConvertableToMessage, extract_number
|
|
6
6
|
from ..message_types import Role, AssistantMsg
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..mcp import MCPConnection
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class DictFromLLMResponse(dict):
|
|
10
13
|
llm_response: "LLMResponse"
|
|
@@ -13,6 +16,9 @@ class DictFromLLMResponse(dict):
|
|
|
13
16
|
self.llm_response = llm_response
|
|
14
17
|
return self
|
|
15
18
|
|
|
19
|
+
async def to_mcp(self, mcp: "MCPConnection"):
|
|
20
|
+
return await mcp.exec(self)
|
|
21
|
+
|
|
16
22
|
|
|
17
23
|
class LLMResponse(ExtendedString, ConvertableToMessage):
|
|
18
24
|
"""
|
|
@@ -52,7 +58,7 @@ class LLMResponse(ExtendedString, ConvertableToMessage):
|
|
|
52
58
|
validator: callable = None,
|
|
53
59
|
) -> list | dict | float | int | str | DictFromLLMResponse:
|
|
54
60
|
try:
|
|
55
|
-
res = parse_json(
|
|
61
|
+
res = super().parse_json(raise_errors=True, required_fields=required_fields)
|
|
56
62
|
if validator:
|
|
57
63
|
try:
|
|
58
64
|
validator(res)
|
|
@@ -83,3 +89,6 @@ class LLMResponse(ExtendedString, ConvertableToMessage):
|
|
|
83
89
|
|
|
84
90
|
def as_message(self) -> AssistantMsg:
|
|
85
91
|
return self.as_assistant
|
|
92
|
+
|
|
93
|
+
async def to_mcp(self, mcp: "MCPConnection"):
|
|
94
|
+
return await mcp.exec(self)
|
|
@@ -5,11 +5,13 @@ build-backend = "flit_core.buildapi"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ai-microcore"
|
|
7
7
|
dynamic = ["description", "version"]
|
|
8
|
-
keywords = ["llm", "large language models", "ai", "similarity search", "ai search", "gpt", "openai"]
|
|
8
|
+
keywords = ["llm", "large language models", "ai", "similarity search", "ai search", "gpt", "openai", "framework", "adapter"]
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Programming Language :: Python :: 3",
|
|
12
12
|
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
13
15
|
"License :: OSI Approved :: MIT License",
|
|
14
16
|
"Operating System :: OS Independent",
|
|
15
17
|
"Intended Audience :: Developers",
|
|
@@ -24,6 +26,7 @@ dependencies = [
|
|
|
24
26
|
"PyYAML~=6.0",
|
|
25
27
|
"chardet~=5.2.0",
|
|
26
28
|
"tiktoken>=0.7.0,<1.0",
|
|
29
|
+
"mcp~=1.9.1"
|
|
27
30
|
]
|
|
28
31
|
requires-python = ">=3.10"
|
|
29
32
|
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import jinja2
|
|
3
|
-
from ..types import TplFunctionType
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def make_jinja2_env(env) -> jinja2.Environment:
|
|
7
|
-
return jinja2.Environment(
|
|
8
|
-
autoescape=env.config.JINJA2_AUTO_ESCAPE,
|
|
9
|
-
loader=jinja2.ChoiceLoader(
|
|
10
|
-
[jinja2.FileSystemLoader(env.config.PROMPT_TEMPLATES_PATH)]
|
|
11
|
-
),
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def make_tpl_function(env) -> TplFunctionType:
|
|
16
|
-
def tpl(file: os.PathLike[str] | str, **kwargs) -> str:
|
|
17
|
-
return env.jinja_env.get_template(file).render(**kwargs)
|
|
18
|
-
|
|
19
|
-
return tpl
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|