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.
Files changed (192) hide show
  1. infra_mvp/base_client.py +29 -0
  2. infra_mvp/base_server.py +68 -0
  3. infra_mvp/monitoring/__init__.py +15 -0
  4. infra_mvp/monitoring/metrics.py +185 -0
  5. infra_mvp/stream/README.md +56 -0
  6. infra_mvp/stream/__init__.py +14 -0
  7. infra_mvp/stream/__main__.py +101 -0
  8. infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
  9. infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
  10. infra_mvp/stream/agents/reference/blocks/action.json +170 -0
  11. infra_mvp/stream/agents/reference/blocks/button.json +66 -0
  12. infra_mvp/stream/agents/reference/blocks/date.json +65 -0
  13. infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
  14. infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
  15. infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
  16. infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
  17. infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
  18. infra_mvp/stream/agents/reference/blocks/table.json +56 -0
  19. infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
  20. infra_mvp/stream/app.py +49 -0
  21. infra_mvp/stream/container.py +112 -0
  22. infra_mvp/stream/schemas/__init__.py +16 -0
  23. infra_mvp/stream/schemas/agent.py +24 -0
  24. infra_mvp/stream/schemas/interaction.py +28 -0
  25. infra_mvp/stream/schemas/session.py +30 -0
  26. infra_mvp/stream/server.py +321 -0
  27. infra_mvp/stream/services/__init__.py +12 -0
  28. infra_mvp/stream/services/agent_service.py +40 -0
  29. infra_mvp/stream/services/event_converter.py +83 -0
  30. infra_mvp/stream/services/session_service.py +247 -0
  31. yera/__init__.py +50 -1
  32. yera/agents/__init__.py +2 -0
  33. yera/agents/context.py +41 -0
  34. yera/agents/dataclasses.py +69 -0
  35. yera/agents/decorator.py +207 -0
  36. yera/agents/discovery.py +124 -0
  37. yera/agents/typing/__init__.py +0 -0
  38. yera/agents/typing/coerce.py +408 -0
  39. yera/agents/typing/utils.py +19 -0
  40. yera/agents/typing/validate.py +206 -0
  41. yera/cli.py +377 -0
  42. yera/config/__init__.py +1 -0
  43. yera/config/config_utils.py +164 -0
  44. yera/config/function_config.py +55 -0
  45. yera/config/logging.py +18 -0
  46. yera/config/tool_config.py +8 -0
  47. yera/config2/__init__.py +8 -0
  48. yera/config2/dataclasses.py +534 -0
  49. yera/config2/keyring.py +270 -0
  50. yera/config2/paths.py +28 -0
  51. yera/config2/read.py +113 -0
  52. yera/config2/setup.py +109 -0
  53. yera/config2/setup_handlers/__init__.py +1 -0
  54. yera/config2/setup_handlers/anthropic.py +126 -0
  55. yera/config2/setup_handlers/azure.py +236 -0
  56. yera/config2/setup_handlers/base.py +125 -0
  57. yera/config2/setup_handlers/llama_cpp.py +205 -0
  58. yera/config2/setup_handlers/ollama.py +157 -0
  59. yera/config2/setup_handlers/openai.py +137 -0
  60. yera/config2/write.py +87 -0
  61. yera/dsl/__init__.py +0 -0
  62. yera/dsl/functions.py +94 -0
  63. yera/dsl/struct.py +20 -0
  64. yera/dsl/workspace.py +79 -0
  65. yera/events/__init__.py +57 -0
  66. yera/events/blocks/__init__.py +68 -0
  67. yera/events/blocks/action.py +57 -0
  68. yera/events/blocks/bar_chart.py +92 -0
  69. yera/events/blocks/base/__init__.py +20 -0
  70. yera/events/blocks/base/base.py +166 -0
  71. yera/events/blocks/base/chart.py +288 -0
  72. yera/events/blocks/base/layout.py +111 -0
  73. yera/events/blocks/buttons.py +37 -0
  74. yera/events/blocks/columns.py +26 -0
  75. yera/events/blocks/container.py +24 -0
  76. yera/events/blocks/date_picker.py +50 -0
  77. yera/events/blocks/exit.py +39 -0
  78. yera/events/blocks/form.py +24 -0
  79. yera/events/blocks/input_echo.py +22 -0
  80. yera/events/blocks/input_request.py +31 -0
  81. yera/events/blocks/line_chart.py +97 -0
  82. yera/events/blocks/markdown.py +67 -0
  83. yera/events/blocks/slider.py +54 -0
  84. yera/events/blocks/spinner.py +55 -0
  85. yera/events/blocks/system_prompt.py +22 -0
  86. yera/events/blocks/table.py +291 -0
  87. yera/events/models/__init__.py +39 -0
  88. yera/events/models/block_data.py +112 -0
  89. yera/events/models/in_event.py +7 -0
  90. yera/events/models/out_event.py +75 -0
  91. yera/events/runtime.py +187 -0
  92. yera/events/stream.py +91 -0
  93. yera/models/__init__.py +0 -0
  94. yera/models/data_classes.py +20 -0
  95. yera/models/llm_atlas_proxy.py +44 -0
  96. yera/models/llm_context.py +99 -0
  97. yera/models/llm_interfaces/__init__.py +0 -0
  98. yera/models/llm_interfaces/anthropic.py +153 -0
  99. yera/models/llm_interfaces/aws_bedrock.py +14 -0
  100. yera/models/llm_interfaces/azure_openai.py +143 -0
  101. yera/models/llm_interfaces/base.py +26 -0
  102. yera/models/llm_interfaces/interface_registry.py +74 -0
  103. yera/models/llm_interfaces/llama_cpp.py +136 -0
  104. yera/models/llm_interfaces/mock.py +29 -0
  105. yera/models/llm_interfaces/ollama_interface.py +118 -0
  106. yera/models/llm_interfaces/open_ai.py +150 -0
  107. yera/models/llm_workspace.py +19 -0
  108. yera/models/model_atlas.py +139 -0
  109. yera/models/model_definition.py +38 -0
  110. yera/models/model_factory.py +33 -0
  111. yera/opaque/__init__.py +9 -0
  112. yera/opaque/base.py +20 -0
  113. yera/opaque/decorator.py +8 -0
  114. yera/opaque/markdown.py +57 -0
  115. yera/opaque/opaque_function.py +25 -0
  116. yera/tools/__init__.py +29 -0
  117. yera/tools/atlas_tool.py +20 -0
  118. yera/tools/base.py +24 -0
  119. yera/tools/decorated_tool.py +18 -0
  120. yera/tools/decorator.py +35 -0
  121. yera/tools/tool_atlas.py +51 -0
  122. yera/tools/tool_utils.py +361 -0
  123. yera/ui/dist/404.html +1 -0
  124. yera/ui/dist/__next.__PAGE__.txt +10 -0
  125. yera/ui/dist/__next._full.txt +23 -0
  126. yera/ui/dist/__next._head.txt +6 -0
  127. yera/ui/dist/__next._index.txt +5 -0
  128. yera/ui/dist/__next._tree.txt +7 -0
  129. yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
  130. yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
  131. yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
  132. yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
  133. yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
  134. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  135. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  136. yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
  137. yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
  138. yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
  139. yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
  140. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_buildManifest.js +11 -0
  141. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_clientMiddlewareManifest.json +1 -0
  142. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_ssgManifest.js +1 -0
  143. yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
  144. yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
  145. yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
  146. yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
  147. yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
  148. yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
  149. yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  150. yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
  151. yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
  152. yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  153. yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  154. yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  155. yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
  156. yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
  157. yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
  158. yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
  159. yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  160. yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  161. yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
  162. yera/ui/dist/_not-found/__next._full.txt +14 -0
  163. yera/ui/dist/_not-found/__next._head.txt +6 -0
  164. yera/ui/dist/_not-found/__next._index.txt +5 -0
  165. yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
  166. yera/ui/dist/_not-found/__next._not-found.txt +4 -0
  167. yera/ui/dist/_not-found/__next._tree.txt +2 -0
  168. yera/ui/dist/_not-found.html +1 -0
  169. yera/ui/dist/_not-found.txt +14 -0
  170. yera/ui/dist/agent-icon.svg +3 -0
  171. yera/ui/dist/favicon.ico +0 -0
  172. yera/ui/dist/file.svg +1 -0
  173. yera/ui/dist/globe.svg +1 -0
  174. yera/ui/dist/index.html +1 -0
  175. yera/ui/dist/index.txt +23 -0
  176. yera/ui/dist/logo/full_logo.png +0 -0
  177. yera/ui/dist/logo/rune_logo.png +0 -0
  178. yera/ui/dist/logo/rune_logo_borderless.png +0 -0
  179. yera/ui/dist/logo/text_logo.png +0 -0
  180. yera/ui/dist/next.svg +1 -0
  181. yera/ui/dist/send.png +0 -0
  182. yera/ui/dist/send_single.png +0 -0
  183. yera/ui/dist/vercel.svg +1 -0
  184. yera/ui/dist/window.svg +1 -0
  185. yera/utils/__init__.py +1 -0
  186. yera/utils/path_utils.py +38 -0
  187. yera-0.2.0.dist-info/METADATA +65 -0
  188. yera-0.2.0.dist-info/RECORD +190 -0
  189. {yera-0.1.0.dist-info → yera-0.2.0.dist-info}/WHEEL +1 -1
  190. yera-0.2.0.dist-info/entry_points.txt +2 -0
  191. yera-0.1.0.dist-info/METADATA +0 -11
  192. 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)