yera 0.1.0__py3-none-any.whl → 0.2.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.
- infra_mvp/base_client.py +29 -0
- infra_mvp/base_server.py +68 -0
- infra_mvp/monitoring/__init__.py +15 -0
- infra_mvp/monitoring/metrics.py +185 -0
- infra_mvp/stream/README.md +56 -0
- infra_mvp/stream/__init__.py +14 -0
- infra_mvp/stream/__main__.py +101 -0
- infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
- infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
- infra_mvp/stream/agents/reference/blocks/action.json +170 -0
- infra_mvp/stream/agents/reference/blocks/button.json +66 -0
- infra_mvp/stream/agents/reference/blocks/date.json +65 -0
- infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
- infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
- infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
- infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
- infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
- infra_mvp/stream/agents/reference/blocks/table.json +56 -0
- infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
- infra_mvp/stream/app.py +49 -0
- infra_mvp/stream/container.py +112 -0
- infra_mvp/stream/schemas/__init__.py +16 -0
- infra_mvp/stream/schemas/agent.py +24 -0
- infra_mvp/stream/schemas/interaction.py +28 -0
- infra_mvp/stream/schemas/session.py +30 -0
- infra_mvp/stream/server.py +321 -0
- infra_mvp/stream/services/__init__.py +12 -0
- infra_mvp/stream/services/agent_service.py +40 -0
- infra_mvp/stream/services/event_converter.py +83 -0
- infra_mvp/stream/services/session_service.py +247 -0
- yera/__init__.py +50 -1
- yera/agents/__init__.py +2 -0
- yera/agents/context.py +41 -0
- yera/agents/dataclasses.py +69 -0
- yera/agents/decorator.py +207 -0
- yera/agents/discovery.py +124 -0
- yera/agents/typing/__init__.py +0 -0
- yera/agents/typing/coerce.py +408 -0
- yera/agents/typing/utils.py +19 -0
- yera/agents/typing/validate.py +206 -0
- yera/cli.py +377 -0
- yera/config/__init__.py +1 -0
- yera/config/config_utils.py +164 -0
- yera/config/function_config.py +55 -0
- yera/config/logging.py +18 -0
- yera/config/tool_config.py +8 -0
- yera/config2/__init__.py +8 -0
- yera/config2/dataclasses.py +534 -0
- yera/config2/keyring.py +270 -0
- yera/config2/paths.py +28 -0
- yera/config2/read.py +113 -0
- yera/config2/setup.py +109 -0
- yera/config2/setup_handlers/__init__.py +1 -0
- yera/config2/setup_handlers/anthropic.py +126 -0
- yera/config2/setup_handlers/azure.py +236 -0
- yera/config2/setup_handlers/base.py +125 -0
- yera/config2/setup_handlers/llama_cpp.py +205 -0
- yera/config2/setup_handlers/ollama.py +157 -0
- yera/config2/setup_handlers/openai.py +137 -0
- yera/config2/write.py +87 -0
- yera/dsl/__init__.py +0 -0
- yera/dsl/functions.py +94 -0
- yera/dsl/struct.py +20 -0
- yera/dsl/workspace.py +79 -0
- yera/events/__init__.py +57 -0
- yera/events/blocks/__init__.py +68 -0
- yera/events/blocks/action.py +57 -0
- yera/events/blocks/bar_chart.py +92 -0
- yera/events/blocks/base/__init__.py +20 -0
- yera/events/blocks/base/base.py +166 -0
- yera/events/blocks/base/chart.py +288 -0
- yera/events/blocks/base/layout.py +111 -0
- yera/events/blocks/buttons.py +37 -0
- yera/events/blocks/columns.py +26 -0
- yera/events/blocks/container.py +24 -0
- yera/events/blocks/date_picker.py +50 -0
- yera/events/blocks/exit.py +39 -0
- yera/events/blocks/form.py +24 -0
- yera/events/blocks/input_echo.py +22 -0
- yera/events/blocks/input_request.py +31 -0
- yera/events/blocks/line_chart.py +97 -0
- yera/events/blocks/markdown.py +67 -0
- yera/events/blocks/slider.py +54 -0
- yera/events/blocks/spinner.py +55 -0
- yera/events/blocks/system_prompt.py +22 -0
- yera/events/blocks/table.py +291 -0
- yera/events/models/__init__.py +39 -0
- yera/events/models/block_data.py +112 -0
- yera/events/models/in_event.py +7 -0
- yera/events/models/out_event.py +75 -0
- yera/events/runtime.py +187 -0
- yera/events/stream.py +91 -0
- yera/models/__init__.py +0 -0
- yera/models/data_classes.py +20 -0
- yera/models/llm_atlas_proxy.py +44 -0
- yera/models/llm_context.py +99 -0
- yera/models/llm_interfaces/__init__.py +0 -0
- yera/models/llm_interfaces/anthropic.py +153 -0
- yera/models/llm_interfaces/aws_bedrock.py +14 -0
- yera/models/llm_interfaces/azure_openai.py +143 -0
- yera/models/llm_interfaces/base.py +26 -0
- yera/models/llm_interfaces/interface_registry.py +74 -0
- yera/models/llm_interfaces/llama_cpp.py +136 -0
- yera/models/llm_interfaces/mock.py +29 -0
- yera/models/llm_interfaces/ollama_interface.py +118 -0
- yera/models/llm_interfaces/open_ai.py +150 -0
- yera/models/llm_workspace.py +19 -0
- yera/models/model_atlas.py +139 -0
- yera/models/model_definition.py +38 -0
- yera/models/model_factory.py +33 -0
- yera/opaque/__init__.py +9 -0
- yera/opaque/base.py +20 -0
- yera/opaque/decorator.py +8 -0
- yera/opaque/markdown.py +57 -0
- yera/opaque/opaque_function.py +25 -0
- yera/tools/__init__.py +29 -0
- yera/tools/atlas_tool.py +20 -0
- yera/tools/base.py +24 -0
- yera/tools/decorated_tool.py +18 -0
- yera/tools/decorator.py +35 -0
- yera/tools/tool_atlas.py +51 -0
- yera/tools/tool_utils.py +361 -0
- yera/ui/dist/404.html +1 -0
- yera/ui/dist/__next.__PAGE__.txt +10 -0
- yera/ui/dist/__next._full.txt +23 -0
- yera/ui/dist/__next._head.txt +6 -0
- yera/ui/dist/__next._index.txt +5 -0
- yera/ui/dist/__next._tree.txt +7 -0
- yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
- yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
- yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
- yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
- yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
- yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
- yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
- yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_buildManifest.js +11 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_clientMiddlewareManifest.json +1 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_ssgManifest.js +1 -0
- yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
- yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
- yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
- yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
- yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
- yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
- yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
- yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
- yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
- yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
- yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
- yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
- yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
- yera/ui/dist/_not-found/__next._full.txt +14 -0
- yera/ui/dist/_not-found/__next._head.txt +6 -0
- yera/ui/dist/_not-found/__next._index.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.txt +4 -0
- yera/ui/dist/_not-found/__next._tree.txt +2 -0
- yera/ui/dist/_not-found.html +1 -0
- yera/ui/dist/_not-found.txt +14 -0
- yera/ui/dist/agent-icon.svg +3 -0
- yera/ui/dist/favicon.ico +0 -0
- yera/ui/dist/file.svg +1 -0
- yera/ui/dist/globe.svg +1 -0
- yera/ui/dist/index.html +1 -0
- yera/ui/dist/index.txt +23 -0
- yera/ui/dist/logo/full_logo.png +0 -0
- yera/ui/dist/logo/rune_logo.png +0 -0
- yera/ui/dist/logo/rune_logo_borderless.png +0 -0
- yera/ui/dist/logo/text_logo.png +0 -0
- yera/ui/dist/next.svg +1 -0
- yera/ui/dist/send.png +0 -0
- yera/ui/dist/send_single.png +0 -0
- yera/ui/dist/vercel.svg +1 -0
- yera/ui/dist/window.svg +1 -0
- yera/utils/__init__.py +1 -0
- yera/utils/path_utils.py +38 -0
- yera-0.2.0.dist-info/METADATA +65 -0
- yera-0.2.0.dist-info/RECORD +190 -0
- {yera-0.1.0.dist-info → yera-0.2.0.dist-info}/WHEEL +1 -1
- yera-0.2.0.dist-info/entry_points.txt +2 -0
- yera-0.1.0.dist-info/METADATA +0 -11
- yera-0.1.0.dist-info/RECORD +0 -4
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from openai import AzureOpenAI
|
|
4
|
+
from openai.lib._pydantic import to_strict_json_schema
|
|
5
|
+
|
|
6
|
+
from yera.config2.dataclasses import LLMConfig
|
|
7
|
+
from yera.config2.keyring import DevKeyring
|
|
8
|
+
from yera.models.data_classes import Message
|
|
9
|
+
from yera.models.llm_interfaces.base import BaseLLMInterface, TBaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AzureOpenAILLM(BaseLLMInterface):
|
|
13
|
+
def __init__(self, config: LLMConfig, overrides, creds_map):
|
|
14
|
+
kws = {**config.inference.model_dump(), **overrides}
|
|
15
|
+
self.client = None
|
|
16
|
+
self._endpoint_path = f"providers.{creds_map['endpoint']}"
|
|
17
|
+
self._api_key_path = f"providers.{creds_map['api_key']}"
|
|
18
|
+
|
|
19
|
+
self.deployment_name = config.model_id
|
|
20
|
+
|
|
21
|
+
super().__init__(**kws)
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
endpoint = DevKeyring.get(self._endpoint_path)
|
|
25
|
+
api_key = DevKeyring.get(self._api_key_path)
|
|
26
|
+
|
|
27
|
+
self.client = AzureOpenAI(
|
|
28
|
+
azure_endpoint=endpoint,
|
|
29
|
+
api_key=api_key,
|
|
30
|
+
api_version="2024-10-21", # Latest GA version
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def stop(self):
|
|
34
|
+
self.client.close()
|
|
35
|
+
self.client = None
|
|
36
|
+
|
|
37
|
+
def chat(
|
|
38
|
+
self,
|
|
39
|
+
messages: list[Message],
|
|
40
|
+
**openai_kw,
|
|
41
|
+
) -> Iterator[str]:
|
|
42
|
+
openai_messages = [
|
|
43
|
+
{"role": msg.role, "content": msg.content} for msg in messages
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
stream = self.client.chat.completions.create(
|
|
47
|
+
model=self.deployment_name,
|
|
48
|
+
messages=openai_messages,
|
|
49
|
+
stream=True,
|
|
50
|
+
**openai_kw,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for chunk in stream:
|
|
54
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
55
|
+
yield chunk.choices[0].delta.content
|
|
56
|
+
|
|
57
|
+
def make_struct(
|
|
58
|
+
self,
|
|
59
|
+
messages: list[Message],
|
|
60
|
+
cls: type[TBaseModel],
|
|
61
|
+
**openai_kw,
|
|
62
|
+
) -> Iterator[str]:
|
|
63
|
+
openai_messages = [
|
|
64
|
+
{"role": msg.role, "content": msg.content} for msg in messages
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
schema = to_strict_json_schema(cls)
|
|
69
|
+
stream = self.client.chat.completions.create(
|
|
70
|
+
model=self.deployment_name,
|
|
71
|
+
messages=openai_messages,
|
|
72
|
+
response_format={
|
|
73
|
+
"type": "json_schema",
|
|
74
|
+
"json_schema": {
|
|
75
|
+
"name": cls.__name__,
|
|
76
|
+
"schema": schema,
|
|
77
|
+
"strict": True,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
stream=True,
|
|
81
|
+
**openai_kw,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for chunk in stream:
|
|
85
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
86
|
+
yield chunk.choices[0].delta.content
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
error_msg = str(e).lower()
|
|
90
|
+
|
|
91
|
+
if any(
|
|
92
|
+
phrase in error_msg
|
|
93
|
+
for phrase in [
|
|
94
|
+
"does not support",
|
|
95
|
+
"not supported",
|
|
96
|
+
"not available",
|
|
97
|
+
"unsupported",
|
|
98
|
+
]
|
|
99
|
+
):
|
|
100
|
+
# Fall back to tool calling
|
|
101
|
+
yield from self._make_struct_via_tools(
|
|
102
|
+
openai_messages, cls, **openai_kw
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
# Re-raise other errors
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
def _make_struct_via_tools(
|
|
109
|
+
self,
|
|
110
|
+
messages: list[dict],
|
|
111
|
+
cls: type[TBaseModel],
|
|
112
|
+
**openai_kw,
|
|
113
|
+
) -> Iterator[str]:
|
|
114
|
+
schema = cls.model_json_schema()
|
|
115
|
+
schema.pop("$defs", None)
|
|
116
|
+
schema.pop("definitions", None)
|
|
117
|
+
|
|
118
|
+
tool = {
|
|
119
|
+
"type": "function",
|
|
120
|
+
"function": {
|
|
121
|
+
"name": "return_structured_data",
|
|
122
|
+
"description": "Returns structured data matching the required schema",
|
|
123
|
+
"parameters": schema,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
stream = self.client.chat.completions.create(
|
|
128
|
+
model=self.deployment_name,
|
|
129
|
+
messages=messages,
|
|
130
|
+
tools=[tool],
|
|
131
|
+
tool_choice={
|
|
132
|
+
"type": "function",
|
|
133
|
+
"function": {"name": "return_structured_data"},
|
|
134
|
+
},
|
|
135
|
+
stream=True,
|
|
136
|
+
**openai_kw,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
for chunk in stream:
|
|
140
|
+
if chunk.choices and chunk.choices[0].delta.tool_calls:
|
|
141
|
+
tool_call = chunk.choices[0].delta.tool_calls[0]
|
|
142
|
+
if tool_call.function and tool_call.function.arguments:
|
|
143
|
+
yield tool_call.function.arguments
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
|
|
4
|
+
from yera.dsl.struct import TBaseModel
|
|
5
|
+
from yera.models.data_classes import Message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseLLMInterface(ABC):
|
|
9
|
+
def __init__(self, **kwargs):
|
|
10
|
+
self.kwargs = dict(kwargs)
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def chat(self, messages: list[Message], **kwargs) -> Iterator[str]:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def make_struct(
|
|
18
|
+
self, messages: list[Message], cls: type[TBaseModel], **kwargs
|
|
19
|
+
) -> Iterator[str]:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def start(self):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def stop(self):
|
|
26
|
+
return
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from importlib.util import find_spec
|
|
3
|
+
|
|
4
|
+
from yera.models.llm_interfaces.base import BaseLLMInterface
|
|
5
|
+
from yera.models.llm_interfaces.mock import MockLLMInterface
|
|
6
|
+
|
|
7
|
+
# Cache for loaded classes
|
|
8
|
+
_loaded_cache: dict[str, type[BaseLLMInterface]] = {}
|
|
9
|
+
_loaders: dict[str, Callable[[], type[BaseLLMInterface]]] = {}
|
|
10
|
+
_initialized = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _lazy_init():
|
|
14
|
+
"""Initialize the registry on first access."""
|
|
15
|
+
global _initialized
|
|
16
|
+
if _initialized:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
_initialized = True
|
|
20
|
+
|
|
21
|
+
# Define all interfaces to check
|
|
22
|
+
interfaces = [
|
|
23
|
+
(
|
|
24
|
+
"llama-cpp",
|
|
25
|
+
"yera.models.llm_interfaces.llama_cpp",
|
|
26
|
+
"LlamaCppLLM",
|
|
27
|
+
"llama_cpp",
|
|
28
|
+
),
|
|
29
|
+
(
|
|
30
|
+
"anthropic-sdk",
|
|
31
|
+
"yera.models.llm_interfaces.anthropic",
|
|
32
|
+
"AnthropicLLM",
|
|
33
|
+
"anthropic",
|
|
34
|
+
),
|
|
35
|
+
("openai-sdk", "yera.models.llm_interfaces.open_ai", "OpenAILLM", "openai"),
|
|
36
|
+
(
|
|
37
|
+
"azure-sdk",
|
|
38
|
+
"yera.models.llm_interfaces.azure_openai",
|
|
39
|
+
"AzureOpenAILLM",
|
|
40
|
+
"openai",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
"ollama",
|
|
44
|
+
"yera.models.llm_interfaces.ollama_interface",
|
|
45
|
+
"OllamaLLM",
|
|
46
|
+
"ollama",
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Check availability and register loaders
|
|
51
|
+
for name, module_path, class_name, check_package in interfaces:
|
|
52
|
+
if find_spec(check_package) is not None:
|
|
53
|
+
|
|
54
|
+
def loader(mod=module_path, cls=class_name, n=name):
|
|
55
|
+
if n not in _loaded_cache:
|
|
56
|
+
module = __import__(mod, fromlist=[cls])
|
|
57
|
+
_loaded_cache[n] = getattr(module, cls)
|
|
58
|
+
return _loaded_cache[n]
|
|
59
|
+
|
|
60
|
+
_loaders[name] = loader
|
|
61
|
+
|
|
62
|
+
# Mock is always available
|
|
63
|
+
_loaders["Mock"] = lambda: MockLLMInterface
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_interface(name: str) -> type[BaseLLMInterface]:
|
|
67
|
+
"""Get an interface class by name, loading it lazily."""
|
|
68
|
+
_lazy_init() # Only runs once, on first call
|
|
69
|
+
|
|
70
|
+
if name not in _loaders:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Interface '{name}' not available. Available: {list(_loaders.keys())}"
|
|
73
|
+
)
|
|
74
|
+
return _loaders[name]()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import redirect_stderr
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from llama_cpp import Llama
|
|
9
|
+
|
|
10
|
+
from yera.config2.dataclasses import LLMConfig
|
|
11
|
+
from yera.config2.keyring import DevKeyring
|
|
12
|
+
from yera.models.data_classes import Message
|
|
13
|
+
from yera.models.llm_interfaces.base import BaseLLMInterface, TBaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assert_started(fn):
|
|
17
|
+
@wraps(fn)
|
|
18
|
+
def wrapper(self, *args, **kwargs):
|
|
19
|
+
if self._model is None:
|
|
20
|
+
raise ValueError("Model not started. Call model.start() first.")
|
|
21
|
+
return fn(self, *args, **kwargs)
|
|
22
|
+
|
|
23
|
+
return wrapper
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LlamaCppLLM(BaseLLMInterface):
|
|
27
|
+
def __init__(self, config: LLMConfig, overrides, creds_map):
|
|
28
|
+
# make kwargs from config and overrides
|
|
29
|
+
kws = {**config.inference.model_dump(), **overrides}
|
|
30
|
+
super().__init__(**kws)
|
|
31
|
+
self._model = None
|
|
32
|
+
models_dir = Path(DevKeyring.get("providers." + creds_map["models_dir"]))
|
|
33
|
+
|
|
34
|
+
with (models_dir / "catalogue.json").open() as f:
|
|
35
|
+
catalogue = json.load(f)
|
|
36
|
+
self.model_path = Path(catalogue[config.id])
|
|
37
|
+
if not self.model_path.exists():
|
|
38
|
+
raise FileNotFoundError(self.model_path)
|
|
39
|
+
|
|
40
|
+
def start(self):
|
|
41
|
+
if self._model:
|
|
42
|
+
raise ValueError("Model already started.")
|
|
43
|
+
|
|
44
|
+
with open(os.devnull, "w") as devnull, redirect_stderr(devnull):
|
|
45
|
+
self._model = Llama(
|
|
46
|
+
model_path=str(self.model_path),
|
|
47
|
+
n_gpu_layers=self.kwargs.get("n_gpu_layers", -1),
|
|
48
|
+
n_ctx=self.kwargs.get("context_length", 8096),
|
|
49
|
+
verbose=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@assert_started
|
|
53
|
+
def stop(self):
|
|
54
|
+
self._model.close()
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _process_stream(stream):
|
|
58
|
+
stream = (x["choices"][0]["delta"] for x in stream)
|
|
59
|
+
stream = (x.get("content") for x in stream)
|
|
60
|
+
stream = (x for x in stream if x is not None)
|
|
61
|
+
return stream
|
|
62
|
+
|
|
63
|
+
@assert_started
|
|
64
|
+
def chat(
|
|
65
|
+
self,
|
|
66
|
+
messages: list[Message],
|
|
67
|
+
*,
|
|
68
|
+
temperature: float = 0.2,
|
|
69
|
+
top_p: float = 0.95,
|
|
70
|
+
top_k: int = 40,
|
|
71
|
+
min_p: float = 0.05,
|
|
72
|
+
typical_p: float = 1.0,
|
|
73
|
+
stop: str | list[str] | None = None,
|
|
74
|
+
seed: int | None = None,
|
|
75
|
+
max_tokens: int | None = None,
|
|
76
|
+
presence_penalty: float = 0.0,
|
|
77
|
+
frequency_penalty: float = 0.0,
|
|
78
|
+
repeat_penalty: float = 1.0,
|
|
79
|
+
**llama_cpp_kw,
|
|
80
|
+
) -> Iterator[str]:
|
|
81
|
+
stream = self._model.create_chat_completion(
|
|
82
|
+
messages,
|
|
83
|
+
temperature=temperature,
|
|
84
|
+
top_p=top_p,
|
|
85
|
+
top_k=top_k,
|
|
86
|
+
min_p=min_p,
|
|
87
|
+
typical_p=typical_p,
|
|
88
|
+
stop=stop or [],
|
|
89
|
+
seed=seed,
|
|
90
|
+
max_tokens=max_tokens,
|
|
91
|
+
presence_penalty=presence_penalty,
|
|
92
|
+
frequency_penalty=frequency_penalty,
|
|
93
|
+
repeat_penalty=repeat_penalty,
|
|
94
|
+
stream=True,
|
|
95
|
+
**llama_cpp_kw,
|
|
96
|
+
)
|
|
97
|
+
return self._process_stream(stream)
|
|
98
|
+
|
|
99
|
+
@assert_started
|
|
100
|
+
def make_struct(
|
|
101
|
+
self,
|
|
102
|
+
messages: list[Message],
|
|
103
|
+
cls: type[TBaseModel],
|
|
104
|
+
*,
|
|
105
|
+
temperature: float = 0.2,
|
|
106
|
+
top_p: float = 0.95,
|
|
107
|
+
top_k: int = 40,
|
|
108
|
+
min_p: float = 0.05,
|
|
109
|
+
typical_p: float = 1.0,
|
|
110
|
+
seed: int | None = None,
|
|
111
|
+
max_tokens: int | None = None,
|
|
112
|
+
presence_penalty: float = 0.0,
|
|
113
|
+
frequency_penalty: float = 0.0,
|
|
114
|
+
repeat_penalty: float = 1.0,
|
|
115
|
+
**llama_cpp_kw,
|
|
116
|
+
) -> Iterator[str]:
|
|
117
|
+
stream = self._model.create_chat_completion(
|
|
118
|
+
messages=messages,
|
|
119
|
+
response_format={
|
|
120
|
+
"type": "json_object",
|
|
121
|
+
"schema": cls.model_json_schema(),
|
|
122
|
+
},
|
|
123
|
+
temperature=temperature,
|
|
124
|
+
top_p=top_p,
|
|
125
|
+
top_k=top_k,
|
|
126
|
+
min_p=min_p,
|
|
127
|
+
typical_p=typical_p,
|
|
128
|
+
seed=seed,
|
|
129
|
+
max_tokens=max_tokens,
|
|
130
|
+
presence_penalty=presence_penalty,
|
|
131
|
+
frequency_penalty=frequency_penalty,
|
|
132
|
+
repeat_penalty=repeat_penalty,
|
|
133
|
+
stream=True,
|
|
134
|
+
**llama_cpp_kw,
|
|
135
|
+
)
|
|
136
|
+
return self._process_stream(stream)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from yera.models.llm_interfaces.base import BaseLLMInterface
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MockLLMInterface(BaseLLMInterface):
|
|
5
|
+
def __init__(self, config, overrides, creds_map):
|
|
6
|
+
args = {**config.inference.model_dump(), **overrides}
|
|
7
|
+
|
|
8
|
+
self.chat_response = args["chat_response"]
|
|
9
|
+
self.struct_response = args["struct_response"]
|
|
10
|
+
self.started = False
|
|
11
|
+
self.stopped = False
|
|
12
|
+
self.chat_calls = []
|
|
13
|
+
self.struct_calls = []
|
|
14
|
+
self.creds_map = creds_map
|
|
15
|
+
super().__init__(**args)
|
|
16
|
+
|
|
17
|
+
def chat(self, messages, **kwargs):
|
|
18
|
+
self.chat_calls.append((messages.copy(), kwargs))
|
|
19
|
+
yield from self.chat_response
|
|
20
|
+
|
|
21
|
+
def make_struct(self, messages, cls, **kwargs):
|
|
22
|
+
self.struct_calls.append((messages.copy(), cls, kwargs))
|
|
23
|
+
yield from self.struct_response
|
|
24
|
+
|
|
25
|
+
def start(self):
|
|
26
|
+
self.started = True
|
|
27
|
+
|
|
28
|
+
def stop(self):
|
|
29
|
+
self.stopped = True
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from ollama import Client, ResponseError
|
|
4
|
+
|
|
5
|
+
from yera.config2.dataclasses import LLMConfig
|
|
6
|
+
from yera.config2.keyring import DevKeyring
|
|
7
|
+
from yera.models.data_classes import Message
|
|
8
|
+
from yera.models.llm_interfaces.base import BaseLLMInterface, TBaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OllamaLLM(BaseLLMInterface):
|
|
12
|
+
def __init__(self, config: LLMConfig, overrides, creds_map):
|
|
13
|
+
kws = {**config.inference.model_dump(), **overrides}
|
|
14
|
+
self.client = None
|
|
15
|
+
self.model_id = config.model_id
|
|
16
|
+
self.creds_map = creds_map
|
|
17
|
+
super().__init__(**kws)
|
|
18
|
+
|
|
19
|
+
def start(self):
|
|
20
|
+
base_url = DevKeyring.get(f"providers.{self.creds_map['base_url']}")
|
|
21
|
+
self.client = Client(host=base_url)
|
|
22
|
+
|
|
23
|
+
# Check if Ollama server is running
|
|
24
|
+
try:
|
|
25
|
+
self.client.list() # Simple check to see if server responds
|
|
26
|
+
except Exception as e:
|
|
27
|
+
raise ConnectionError(
|
|
28
|
+
f"Cannot connect to Ollama server at {base_url}. "
|
|
29
|
+
f"Make sure Ollama is installed and running. "
|
|
30
|
+
f"Error: {e}"
|
|
31
|
+
) from None
|
|
32
|
+
|
|
33
|
+
def stop(self):
|
|
34
|
+
self.client = None
|
|
35
|
+
|
|
36
|
+
def chat(
|
|
37
|
+
self,
|
|
38
|
+
messages: list[Message],
|
|
39
|
+
**ollama_kw,
|
|
40
|
+
) -> Iterator[str]:
|
|
41
|
+
ollama_messages = [
|
|
42
|
+
{"role": msg.role, "content": msg.content} for msg in messages
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
stream = self.client.chat(
|
|
47
|
+
model=self.model_id,
|
|
48
|
+
messages=ollama_messages,
|
|
49
|
+
stream=True,
|
|
50
|
+
**ollama_kw,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
in_thought_block = False
|
|
54
|
+
for chunk in stream:
|
|
55
|
+
thought_token = chunk.get("message", {}).get("thinking")
|
|
56
|
+
if thought_token:
|
|
57
|
+
if not in_thought_block:
|
|
58
|
+
yield "\n<thinking>\n"
|
|
59
|
+
in_thought_block = True
|
|
60
|
+
yield thought_token
|
|
61
|
+
content_token = chunk.get("message", {}).get("content")
|
|
62
|
+
if content_token:
|
|
63
|
+
if in_thought_block:
|
|
64
|
+
yield "\n<thinking>\n\n"
|
|
65
|
+
in_thought_block = False
|
|
66
|
+
|
|
67
|
+
yield content_token
|
|
68
|
+
|
|
69
|
+
except ResponseError as e:
|
|
70
|
+
if e.status_code == 404:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Model '{self.model_id}' not found. "
|
|
73
|
+
f"Pull it with: ollama pull {self.model_id}"
|
|
74
|
+
) from e
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
def make_struct(
|
|
78
|
+
self,
|
|
79
|
+
messages: list[Message],
|
|
80
|
+
cls: type[TBaseModel],
|
|
81
|
+
**ollama_kw,
|
|
82
|
+
) -> Iterator[str]:
|
|
83
|
+
ollama_messages = [
|
|
84
|
+
{"role": msg.role, "content": msg.content} for msg in messages
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Use Pydantic's model_json_schema() for structured outputs
|
|
89
|
+
stream = self.client.chat(
|
|
90
|
+
model=self.model_id,
|
|
91
|
+
messages=ollama_messages,
|
|
92
|
+
format=cls.model_json_schema(),
|
|
93
|
+
stream=True,
|
|
94
|
+
**ollama_kw,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
for chunk in stream:
|
|
98
|
+
if not chunk.get("message"):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
msg = chunk["message"]
|
|
102
|
+
|
|
103
|
+
# For structured outputs, thinking is usually separate
|
|
104
|
+
# but we will yield it if present once it won't bugger the JSON.
|
|
105
|
+
# if msg.get("thinking"):
|
|
106
|
+
# yield msg["thinking"]
|
|
107
|
+
|
|
108
|
+
# Yield JSON content
|
|
109
|
+
if msg.get("content"):
|
|
110
|
+
yield msg["content"]
|
|
111
|
+
|
|
112
|
+
except ResponseError as e:
|
|
113
|
+
if e.status_code == 404:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Model '{self.model_id}' not found. "
|
|
116
|
+
f"Pull it with: ollama pull {self.model_id}"
|
|
117
|
+
) from e
|
|
118
|
+
raise
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
|
|
4
|
+
from openai import OpenAI
|
|
5
|
+
from openai.lib._pydantic import to_strict_json_schema
|
|
6
|
+
|
|
7
|
+
from yera.config2.dataclasses import LLMConfig
|
|
8
|
+
from yera.config2.keyring import DevKeyring
|
|
9
|
+
from yera.models.data_classes import Message
|
|
10
|
+
from yera.models.llm_interfaces.base import BaseLLMInterface, TBaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAILLM(BaseLLMInterface):
|
|
14
|
+
def __init__(self, config: LLMConfig, overrides: dict, creds_map: dict[str, str]):
|
|
15
|
+
kws = {**config.inference.model_dump(), **overrides}
|
|
16
|
+
self.client = None
|
|
17
|
+
self.model_id = config.model_id
|
|
18
|
+
self.struct_gen = self._supports_structured_outputs(self.model_id)
|
|
19
|
+
self.creds_map = creds_map
|
|
20
|
+
super().__init__(**kws)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _supports_structured_outputs(model_id: str) -> bool:
|
|
24
|
+
"""Check if a model supports structured outputs via response_format.
|
|
25
|
+
|
|
26
|
+
Structured outputs via response_format are supported on:
|
|
27
|
+
- gpt-4o models from 2024-08-06 onwards
|
|
28
|
+
- gpt-4o-mini models from 2024-07-18 onwards
|
|
29
|
+
- gpt-5+ and o3/o4 models
|
|
30
|
+
|
|
31
|
+
Older models can use tool calling with strict: true instead.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
model_id: Model identifier (e.g., "gpt-4o-2024-08-06")
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if model supports response_format structured outputs
|
|
38
|
+
"""
|
|
39
|
+
# GPT-5+ models support structured outputs
|
|
40
|
+
if (
|
|
41
|
+
model_id.startswith("gpt-5")
|
|
42
|
+
or model_id.startswith("o3")
|
|
43
|
+
or model_id.startswith("o4")
|
|
44
|
+
):
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
# Check gpt-4o and gpt-4o-mini with date snapshots
|
|
48
|
+
gpt4o_pattern = r"gpt-4o(?:-mini)?-(\d{4})-(\d{2})-(\d{2})"
|
|
49
|
+
match = re.search(gpt4o_pattern, model_id)
|
|
50
|
+
|
|
51
|
+
if match:
|
|
52
|
+
year = int(match.group(1))
|
|
53
|
+
month = int(match.group(2))
|
|
54
|
+
day = int(match.group(3))
|
|
55
|
+
date_tuple = (year, month, day)
|
|
56
|
+
|
|
57
|
+
# gpt-4o-mini: supported from 2024-07-18+
|
|
58
|
+
if "mini" in model_id:
|
|
59
|
+
return date_tuple >= (2024, 7, 18)
|
|
60
|
+
# gpt-4o: supported from 2024-08-06+
|
|
61
|
+
return date_tuple >= (2024, 8, 6)
|
|
62
|
+
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def start(self):
|
|
66
|
+
api_key = DevKeyring.get("providers." + self.creds_map["api_key"])
|
|
67
|
+
self.client = OpenAI(api_key=api_key)
|
|
68
|
+
|
|
69
|
+
def stop(self):
|
|
70
|
+
self.client = None
|
|
71
|
+
|
|
72
|
+
def chat(
|
|
73
|
+
self,
|
|
74
|
+
messages: list[Message],
|
|
75
|
+
**openai_kw,
|
|
76
|
+
) -> Iterator[str]:
|
|
77
|
+
openai_messages = [
|
|
78
|
+
{"role": msg.role, "content": msg.content} for msg in messages
|
|
79
|
+
]
|
|
80
|
+
stream = self.client.chat.completions.create(
|
|
81
|
+
model=self.model_id,
|
|
82
|
+
messages=openai_messages,
|
|
83
|
+
stream=True,
|
|
84
|
+
**openai_kw,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for chunk in stream:
|
|
88
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
89
|
+
yield chunk.choices[0].delta.content
|
|
90
|
+
|
|
91
|
+
def make_struct(
|
|
92
|
+
self,
|
|
93
|
+
messages: list[Message],
|
|
94
|
+
cls: type[TBaseModel],
|
|
95
|
+
**openai_kw,
|
|
96
|
+
) -> Iterator[str]:
|
|
97
|
+
openai_messages = [
|
|
98
|
+
{"role": msg.role, "content": msg.content} for msg in messages
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
if self.struct_gen:
|
|
102
|
+
schema = to_strict_json_schema(cls)
|
|
103
|
+
|
|
104
|
+
stream = self.client.chat.completions.create(
|
|
105
|
+
model=self.model_id,
|
|
106
|
+
messages=openai_messages,
|
|
107
|
+
response_format={
|
|
108
|
+
"type": "json_schema",
|
|
109
|
+
"json_schema": {
|
|
110
|
+
"name": cls.__name__,
|
|
111
|
+
"schema": schema,
|
|
112
|
+
"strict": True,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
stream=True,
|
|
116
|
+
**openai_kw,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for chunk in stream:
|
|
120
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
121
|
+
yield chunk.choices[0].delta.content
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
tool_schema = {
|
|
125
|
+
"type": "function",
|
|
126
|
+
"function": {
|
|
127
|
+
"name": "return_structured_output",
|
|
128
|
+
"description": f"Returns structured data conforming to the {cls.__name__} schema",
|
|
129
|
+
"parameters": cls.model_json_schema(),
|
|
130
|
+
"strict": True,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
stream = self.client.chat.completions.create(
|
|
135
|
+
model=self.model_id,
|
|
136
|
+
messages=openai_messages,
|
|
137
|
+
tools=[tool_schema],
|
|
138
|
+
tool_choice={
|
|
139
|
+
"type": "function",
|
|
140
|
+
"function": {"name": "return_structured_output"},
|
|
141
|
+
},
|
|
142
|
+
stream=True,
|
|
143
|
+
**openai_kw,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
for chunk in stream:
|
|
147
|
+
if chunk.choices and chunk.choices[0].delta.tool_calls:
|
|
148
|
+
for tool_call in chunk.choices[0].delta.tool_calls:
|
|
149
|
+
if tool_call.function and tool_call.function.arguments:
|
|
150
|
+
yield tool_call.function.arguments
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from yera.models.data_classes import Message
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LLMWorkspace:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.messages: list[Message] = []
|
|
7
|
+
self.variables: dict = {}
|
|
8
|
+
|
|
9
|
+
def add_user_message(self, content: str | dict):
|
|
10
|
+
message = Message.user(content)
|
|
11
|
+
self.messages.append(message)
|
|
12
|
+
|
|
13
|
+
def add_sys_message(self, content: str | dict):
|
|
14
|
+
message = Message.system(content)
|
|
15
|
+
self.messages.append(message)
|
|
16
|
+
|
|
17
|
+
def add_assistant_message(self, content: str | dict):
|
|
18
|
+
message = Message.assistant(content)
|
|
19
|
+
self.messages.append(message)
|