pydantic-ai-kernel 1.5.2__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.
@@ -0,0 +1,6 @@
1
+ """A wrapper of pydantic-ai in jupyter kernel"""
2
+
3
+ from .kernel import PydanticAIBaseKernel # noqa: F401
4
+ from .jupydantic_agent import JuPydanticAgent
5
+ from .agent_config import AgentConfig, ModelProviderConfig, ModelConfig # noqa: F401
6
+ from .boosted_magic import BoostedMagic, boosted_option, complete_from_list
@@ -0,0 +1,4 @@
1
+ from . import PydanticAIBaseKernel
2
+
3
+
4
+ PydanticAIBaseKernel.run_as_main()
@@ -0,0 +1,330 @@
1
+ from pydantic_ai import UserError
2
+ from pydantic_ai.models import infer_model, Model
3
+ from pydantic_ai.providers import infer_provider_class, Provider
4
+ from typing import Annotated, Any, Literal, Optional, Dict
5
+ from pydantic import BaseModel, Field, field_validator
6
+ import json
7
+ import os
8
+
9
+ AllModelKind = Literal[
10
+ "test",
11
+ "openai",
12
+ "azure",
13
+ "deepseek",
14
+ "fireworks",
15
+ "github",
16
+ "grok",
17
+ "heroku",
18
+ "moonshotai",
19
+ "ollama",
20
+ "together",
21
+ "vercel",
22
+ "litellm",
23
+ "nebius",
24
+ "ovhcloud",
25
+ "alibaba",
26
+ "openai-chat",
27
+ "openai-responses",
28
+ "google-gla",
29
+ "google-vertex",
30
+ "google",
31
+ "groq",
32
+ "cohere",
33
+ "mistral",
34
+ "openrouter",
35
+ "anthropic",
36
+ "bedrock",
37
+ "huggingface",
38
+ "cerebras",
39
+ ]
40
+
41
+
42
+ def infer_model_type( # noqa: C901
43
+ model_kind: str,
44
+ ) -> type[Model]:
45
+ """Infer the model type from the name.
46
+ Function extracted and modified from pydantic-ai `infer_model`.
47
+
48
+ Args:
49
+ model_kind:
50
+ Kind of model to instantiate. Use the string "test" to instantiate TestModel.
51
+ """
52
+ if model_kind == "test":
53
+ from pydantic_ai.models.test import TestModel
54
+
55
+ return TestModel
56
+
57
+ if model_kind in (
58
+ "openai",
59
+ "azure",
60
+ "deepseek",
61
+ "fireworks",
62
+ "github",
63
+ "grok",
64
+ "heroku",
65
+ "moonshotai",
66
+ "ollama",
67
+ "together",
68
+ "vercel",
69
+ "litellm",
70
+ "nebius",
71
+ "ovhcloud",
72
+ "alibaba",
73
+ ):
74
+ model_kind = "openai-chat"
75
+ elif model_kind in ("google-gla", "google-vertex"):
76
+ model_kind = "google"
77
+
78
+ if model_kind == "openai-chat":
79
+ from pydantic_ai.models.openai import OpenAIChatModel
80
+
81
+ return OpenAIChatModel
82
+ elif model_kind == "openai-responses":
83
+ from pydantic_ai.models.openai import OpenAIResponsesModel
84
+
85
+ return OpenAIResponsesModel
86
+ elif model_kind == "google":
87
+ from pydantic_ai.models.google import GoogleModel
88
+
89
+ return GoogleModel
90
+ elif model_kind == "groq":
91
+ from pydantic_ai.models.groq import GroqModel
92
+
93
+ return GroqModel
94
+ elif model_kind == "cohere":
95
+ from pydantic_ai.models.cohere import CohereModel
96
+
97
+ return CohereModel
98
+ elif model_kind == "mistral":
99
+ from pydantic_ai.models.mistral import MistralModel
100
+
101
+ return MistralModel
102
+ elif model_kind == "openrouter":
103
+ from pydantic_ai.models.openrouter import OpenRouterModel
104
+
105
+ return OpenRouterModel
106
+ elif model_kind == "anthropic":
107
+ from pydantic_ai.models.anthropic import AnthropicModel
108
+
109
+ return AnthropicModel
110
+ elif model_kind == "bedrock":
111
+ from pydantic_ai.models.bedrock import BedrockConverseModel
112
+
113
+ return BedrockConverseModel
114
+ elif model_kind == "huggingface":
115
+ from pydantic_ai.models.huggingface import HuggingFaceModel
116
+
117
+ return HuggingFaceModel
118
+ elif model_kind == "cerebras":
119
+ from pydantic_ai.models.cerebras import CerebrasModel
120
+
121
+ return CerebrasModel
122
+ else:
123
+ raise UserError(f"Unknown model: {model_kind}") # pragma: no cover
124
+
125
+
126
+ class ModelProviderConfig(BaseModel):
127
+ name: Annotated[
128
+ str,
129
+ Field(
130
+ description="Name of the inference provider. The complete list is the one from pydantic-ai. https://ai.pydantic.dev/api/providers/",
131
+ examples=[
132
+ "ollama",
133
+ "litellm",
134
+ "anthropic",
135
+ "nebius",
136
+ "alibaba",
137
+ "ovhcloud",
138
+ ],
139
+ ),
140
+ ]
141
+ params: Annotated[
142
+ dict,
143
+ Field(
144
+ description="The parameters that will be given to pydantic-ai Provider class. Depends on provider name. Go to : https://ai.pydantic.dev/models/overview/",
145
+ examples=[{"base_url": "http://localhost:11434/v1"}],
146
+ ),
147
+ ]
148
+
149
+ @property
150
+ def get_provider(self) -> Provider[Any]:
151
+ ProviderConstructor = infer_provider_class(self.name)
152
+ return ProviderConstructor(**self.params)
153
+
154
+
155
+ class ModelConfig(BaseModel):
156
+ """
157
+ Describe a model and its inference provider. Is given to pydantic-ai
158
+ agent.
159
+ """
160
+
161
+ model_type: AllModelKind | None = None
162
+ model_name: Annotated[
163
+ str,
164
+ Field(
165
+ description="""Following pydantic-ai implementation, there are two ways
166
+ to write model name :
167
+ - <provider_name>:<model_name>, example 'ollama:qwen3:1.7b
168
+ - <model_name>
169
+
170
+ In the first one, you CAN provide additionnal informations on the provider within
171
+ the field 'provider' below. They will **erase** the ones infered by pydantic-ai.
172
+ A wide range of values is possible here, and you can refer to pydantic-ai documentation :
173
+ https://ai.pydantic.dev/models/overview/
174
+
175
+ In the second one, you MUST provide additionnal informations on the provider within
176
+ the field 'provider' below.""",
177
+ examples=["ollama:qwen3:1.7b", "qwen3.1:1.7b"],
178
+ ),
179
+ ]
180
+ model_settings: Annotated[
181
+ dict | None,
182
+ Field(
183
+ description="Settings for the model. Depends on model and inference provider. See https://ai.pydantic.dev/api/settings/#pydantic_ai.settings.ModelSettings",
184
+ default=None,
185
+ ),
186
+ ]
187
+ model_provider: Annotated[
188
+ ModelProviderConfig | None,
189
+ Field(
190
+ description="Description of the inference provider. When specified, inferred information about the provider from model_name will be erased.",
191
+ default=None,
192
+ ),
193
+ ]
194
+
195
+ @property
196
+ def get_model(self) -> Model | None:
197
+ if self.model_type is None:
198
+ try:
199
+ return infer_model(self.model_name)
200
+ except Exception as e:
201
+ raise Exception(
202
+ f"Could not access model. See error above or consider adding at least information about the model type in the configuration file. Model types are : {AllModelKind}."
203
+ ) from e
204
+ if self.model_provider is None:
205
+ try:
206
+ return infer_model(self.model_name)
207
+ except Exception as e:
208
+ raise Exception(
209
+ "Could not access model. See error above or consider adding at least a model provider in configuration file"
210
+ ) from e
211
+
212
+ provider = self.model_provider.get_provider
213
+ ModelConstructor = infer_model_type(self.model_type)
214
+ return ModelConstructor(self.model_name, provider=provider) # pyright: ignore
215
+
216
+
217
+ class AgentConfig(BaseModel):
218
+ agent_name: Annotated[
219
+ str,
220
+ Field(
221
+ description="The name of the agent within the application - different from model name.",
222
+ examples=["my-agent", "summarizer", "image-generator"],
223
+ ),
224
+ ]
225
+ system_prompt: Annotated[
226
+ str,
227
+ Field(
228
+ description="The system prompt for this agent, or a path towards a text file containing system prompt.",
229
+ examples=[
230
+ "You are a nice agent who answers nicely to requests of the user",
231
+ "You are a specialist in cooking and are always willing to provide informations on new cooking recipees.",
232
+ "/Users/mgg/Documents/system_prompt.txt",
233
+ "/Users/mgg/Documents/system_prompt.md",
234
+ ],
235
+ ),
236
+ ]
237
+ model: ModelConfig
238
+ mcp_servers: Annotated[
239
+ Optional[Dict[str, Any]],
240
+ Field(
241
+ description="A dictionnary of MCP server configuration to connect to. Keys are MCP server names.",
242
+ examples=[
243
+ {
244
+ "time_mcp_server": {
245
+ "command": "uvx",
246
+ "args": ["mcp-run-python", "stdio"],
247
+ },
248
+ "weather_server": {"command": "python", "args": ["mcp_server.py"]},
249
+ },
250
+ {
251
+ "python-runner": {
252
+ "command": "uv",
253
+ "args": ["run", "mcp-run-python", "stdio"],
254
+ },
255
+ "weather": {"command": "python", "args": ["mcp_server.py"]},
256
+ "weather-api": {"url": "http://localhost:3001/sse"},
257
+ "calculator": {"url": "http://localhost:8000/mcp"},
258
+ },
259
+ ],
260
+ ),
261
+ ] = None
262
+ mcp_servers_user_approval: Annotated[
263
+ Optional[Dict[str, bool | dict]],
264
+ Field(
265
+ description="For each MCP server, information about whether the tool requires user approval or not. Can be either a boolean : True = All tools in the MCP server requires user validation (False = all tools **don't** requires validation). Or can be a dict where keys are tool names, and values boolean. When not specified, a tool needs user validation by default.",
266
+ examples=[
267
+ {
268
+ "python-runner": True,
269
+ "weather-api": {"get_weather": True, "authenticate_to_api": False},
270
+ "calculator": False,
271
+ }
272
+ ],
273
+ ),
274
+ ] = None
275
+ display_thinking: Annotated[
276
+ bool,
277
+ Field(
278
+ description="Whether or not to display thinking text of the model. Default to true."
279
+ ),
280
+ ] = True
281
+ formatter: Annotated[
282
+ str,
283
+ Field(
284
+ description="Output formatter for magics (text, terminal, md, json).",
285
+ examples=["text", "terminal", "md", "json"],
286
+ ),
287
+ ] = "terminal"
288
+ use_widget: Annotated[
289
+ bool,
290
+ Field(
291
+ description="Whether to use ipywidget in frontend for enhanced displaying"
292
+ ),
293
+ ] = False
294
+
295
+ @field_validator("system_prompt", mode="after")
296
+ @classmethod
297
+ def load_system_prompt(cls, value: str):
298
+ """
299
+ If system prompt argument is a path, loads it.
300
+ Else, does nothing.
301
+ """
302
+ if os.path.isfile(value):
303
+ with open(value, "rt") as f:
304
+ o = f.read()
305
+ return o
306
+ return value
307
+
308
+ @field_validator("display_thinking", mode="after")
309
+ @classmethod
310
+ def validate_display_thinking(cls, value):
311
+ # env variable overrides config value
312
+ if os.getenv("PYDANTIC_AI_KERNEL_DISPLAY_THINKING", False):
313
+ return True
314
+ return value
315
+
316
+ @field_validator("formatter", mode="after")
317
+ @classmethod
318
+ def validate_formatter(cls, value):
319
+ # env variable overrides config value; fallback to default if not set
320
+ env = os.getenv("PYDANTIC_AI_KERNEL_FORMATTER", False)
321
+ if env:
322
+ return env
323
+ return value
324
+
325
+
326
+ if __name__ == "__main__":
327
+ import json
328
+
329
+ with open("config_scheme.json", "wt") as f:
330
+ json.dump(AgentConfig.model_json_schema(), f)
@@ -0,0 +1,256 @@
1
+ from metakernel import Magic
2
+ from metakernel.magic import (
3
+ _indent,
4
+ _split_args,
5
+ )
6
+ from typing import Any, TypeVar, Callable
7
+ import inspect
8
+ import traceback
9
+ import argparse
10
+ from statikomand import KomandParser
11
+
12
+ _F = TypeVar("_F", bound=Callable[..., Any])
13
+
14
+
15
+ class BoostedMagic(Magic):
16
+ """
17
+ Subclass of metakernel Magics, that provides additional
18
+ features :
19
+ - completion for options names and values, based
20
+ on custom completer objects
21
+ - parsing based on argparse
22
+
23
+ To implement custom parser for magic line, the get_args
24
+ and call_magic had to be reimplemented.
25
+ """
26
+
27
+ def __init__(self, *args, **kwargs) -> None:
28
+ super().__init__(*args, **kwargs)
29
+
30
+ def get_args(self, mtype: str, name: str, code: str, args: Any) -> Any:
31
+ self.code = code
32
+ old_args = args
33
+ mtype = mtype.replace("sticky", "cell")
34
+
35
+ func = getattr(self, mtype + "_" + name)
36
+
37
+ if "-h" in args or "--help" in args:
38
+ # --help flag of argparse writes to stdout,
39
+ # which break kernels (do_is_complete message is
40
+ # unanswered, which makes the kernel to crash).
41
+ return ["help"], dict(), list()
42
+
43
+ try:
44
+ args, kwargs = _boosted_parse_args(
45
+ func, args, usage=self.get_help(mtype, name)
46
+ )
47
+ except Exception as e:
48
+ self.kernel.Error(str(e))
49
+ return self
50
+ arg_spec = inspect.getfullargspec(func)
51
+ fargs = arg_spec.args
52
+ if fargs[0] == "self":
53
+ fargs = fargs[1:]
54
+
55
+ fargs = [f for f in fargs if f not in kwargs.keys()]
56
+ if len(args) > len(fargs) and not arg_spec.varargs:
57
+ extra = " ".join(str(s) for s in (args[len(fargs) - 1 :]))
58
+ args = args[: len(fargs) - 1] + [extra]
59
+
60
+ return (args, kwargs, old_args)
61
+
62
+ def call_magic(self, mtype: str, name: str, code: str, args: Any) -> Magic:
63
+ self.code = code
64
+ old_args = args
65
+ mtype = mtype.replace("sticky", "cell")
66
+
67
+ func = getattr(self, mtype + "_" + name)
68
+ try:
69
+ args, kwargs = _boosted_parse_args(
70
+ func, args, usage=self.get_help(mtype, name)
71
+ )
72
+ except Exception as e:
73
+ self.kernel.Error(str(e))
74
+ return self
75
+
76
+ arg_spec = inspect.getfullargspec(func)
77
+ fargs = arg_spec.args
78
+ if fargs[0] == "self":
79
+ fargs = fargs[1:]
80
+
81
+ fargs = [f for f in fargs if f not in kwargs.keys()]
82
+ if len(args) > len(fargs) and not arg_spec.varargs:
83
+ extra = " ".join(str(s) for s in (args[len(fargs) - 1 :]))
84
+ args = args[: len(fargs) - 1] + [extra]
85
+
86
+ try:
87
+ try:
88
+ func(*args, **kwargs)
89
+ except TypeError:
90
+ func(old_args)
91
+ except Exception as exc:
92
+ msg = f"Error in calling magic '{name}' on {mtype}:\n {exc!s}\n args: {args}\n kwargs: {kwargs}"
93
+ self.kernel.Error(msg)
94
+ self.kernel.Error(traceback.format_exc())
95
+ self.kernel.Error(self.get_help(mtype, name))
96
+ # return dummy magic to end processing:
97
+ return Magic(self.kernel)
98
+ return self
99
+
100
+ def get_completions(self, info: dict[str, Any]) -> list[str]:
101
+ """
102
+ Uses statikomand argparse subclass to complete the current magic.
103
+ """
104
+ self.kernel.log.info(f"Info boosted magic : {info}")
105
+ func = None
106
+ for each_attr in dir(self):
107
+ if each_attr.startswith("line"):
108
+ func = getattr(self, each_attr)
109
+ break
110
+ # for now, info does not contains magic type
111
+ # if each_attr.startswith("cell"):
112
+ # func = getattr(self, each_attr)
113
+ # break
114
+ if func is None:
115
+ self.kernel.log.warning("Could not find any magic to complete.")
116
+ return []
117
+ if not getattr(func, "has_boosted_options", False):
118
+ self.kernel.log.debug(
119
+ "No boosted options found for magic, hence not completing."
120
+ )
121
+ return []
122
+ boosted_parser: KomandParser | bool = getattr(func, "boosted_parser", False)
123
+ if not boosted_parser:
124
+ self.kernel.log.warning(
125
+ "Could not find boosted parser for magic, but has_boosted_options is True. Not completing."
126
+ )
127
+ return []
128
+ args = info["code"]
129
+ if isinstance(args, list):
130
+ args = " ".join(args)
131
+
132
+ args = _split_args(args)
133
+ if not isinstance(boosted_parser, KomandParser):
134
+ raise TypeError(
135
+ f"boosted_parser parameter of magic method must be a KomandParser, not {type(boosted_parser)}."
136
+ )
137
+
138
+ # finally calls complete method of boosted parser
139
+ try:
140
+ completed = boosted_parser.complete(args)
141
+ except Exception:
142
+ self.kernel.log.debug(
143
+ f"Error when completing option : {traceback.format_exc()}"
144
+ )
145
+ return []
146
+
147
+ if len(args) > 0:
148
+ self.kernel.log.info(f"Modifying -- and - tokens : {completed}")
149
+ result = []
150
+ last_token = args[-1]
151
+ self.kernel.log.info(f"last token : {last_token}")
152
+ for each_match in completed:
153
+ if each_match == last_token:
154
+ continue
155
+ # for option tokens (starting with '-'), metakernel does
156
+ # not replace the full token, but adds the completion.
157
+ # so we need to truncate it. Somehow, tokens starting
158
+ # with '-' are not treated as others.
159
+ # For other tokens, it works as statikomand.
160
+ if last_token.startswith("--") and each_match.startswith("--"):
161
+ each_match = each_match[2:]
162
+ elif last_token.startswith("-") and each_match.startswith("-"):
163
+ each_match = each_match[1:]
164
+
165
+ result.append(each_match)
166
+ else:
167
+ result = completed
168
+ self.kernel.log.debug(f"Boosted magic completion : {result}")
169
+ return result
170
+
171
+
172
+ def boosted_option(*args: Any, **kwargs: Any) -> Callable[[_F], _F]:
173
+ """
174
+ Return decorator that adds a boosted magic option to a function.
175
+ The boosted magic uses argparse instead of optparse, and
176
+ allows for tab completion among options names and values.
177
+ """
178
+
179
+ def decorator(func: _F) -> _F:
180
+ help_text = ""
181
+ if not getattr(func, "has_boosted_options", False):
182
+ func.has_boosted_options = True # type:ignore[attr-defined]
183
+ func.boosted_parser = KomandParser(prog=func.__name__, add_help=False) # type:ignore[attr-defined]
184
+ # boosted_parser is an instance of argparse.ArgumentParser
185
+ # add_help is set to False because argparse help writes to stdout, which
186
+ # breaks kernel machinery
187
+ help_text += "Options:\n-------\n"
188
+
189
+ try:
190
+ action = func.boosted_parser.add_argument(*args, **kwargs) # type:ignore[attr-defined]
191
+ except AttributeError:
192
+ pass
193
+ except argparse.ArgumentError:
194
+ help_text += args[0] + "\n"
195
+ else:
196
+ help_text += _format_action(action) + "\n"
197
+ if func.__doc__:
198
+ func.__doc__ += _indent(func.__doc__, help_text)
199
+ else:
200
+ func.__doc__ = help_text
201
+ return func
202
+
203
+ return decorator
204
+
205
+
206
+ def _boosted_parse_args(
207
+ func, args: str | list[str], usage
208
+ ) -> tuple[list[Any], dict[str, Any]]:
209
+
210
+ if isinstance(args, list):
211
+ args = " ".join(args)
212
+ args = _split_args(args)
213
+
214
+ # raise ValueError(f"dajkbz {args}")
215
+ if getattr(func, "has_boosted_options", False) and getattr(
216
+ func, "boosted_parser", False
217
+ ):
218
+ if not isinstance(func.boosted_parser, argparse.ArgumentParser):
219
+ return list(), dict()
220
+ parsed_args = func.boosted_parser.parse_args(args)
221
+ return parsed_args._get_args(), parsed_args.__dict__
222
+ return list(), dict()
223
+
224
+
225
+ def _format_action(action: argparse.Action):
226
+ out = ""
227
+ out = ", ".join(action.option_strings) + f" : {action.help}"
228
+ return out
229
+
230
+
231
+ def complete_from_list(
232
+ list_of_reference: list[str],
233
+ word: str,
234
+ rank: int = 0,
235
+ ):
236
+ """
237
+ Completes a word from a given list_of_reference.
238
+ Return all occurences of list_of_reference whose
239
+ beginning matches word.
240
+ Can be used as within a lambda function with a custom list:
241
+
242
+ ```python
243
+ my_list = ["bobo", "baba"]
244
+ completer = lambda w,r : complete_from_list(my_list, w, r)
245
+ ```
246
+
247
+ """
248
+ all_matches = []
249
+
250
+ for each_word in list_of_reference:
251
+ if len(each_word) < len(word):
252
+ continue
253
+ potential_match = each_word[: len(word)]
254
+ if potential_match == word:
255
+ all_matches.append(each_word)
256
+ return all_matches