llmalchemy 2.0.0__tar.gz → 3.0.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llmalchemy
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: A full-access agent pattern.
5
5
  Author: Ignacio Llorca
6
6
  Author-email: Ignacio Llorca <nllorca@proton.me>
@@ -178,10 +178,10 @@ MessageEvent(message=UserMessage(content="Code rejected: Forbidden import: os"))
178
178
  <details>
179
179
  <summary>Custom system prompt</summary>
180
180
  You can pass a Jinja template to override the default one (see `src/llmalchemy/prompt.jinja`).
181
- It is recommended that the template contains vars {{ SCHEMA }}, {{ SYMBOLS }}, {{ TOOLS }} and {{ IMPORTS }}.
181
+ It is recommended that the template contains vars {{ SCHEMA }}, {{ TOOLS }} and {{ IMPORTS }}.
182
182
 
183
183
  ```python
184
- prompt = """Write python code to answer user requests. You have access to {{ SCHEMA }}, {{ SYMBOLS }} and {{ TOOLS }}."""
184
+ prompt = """Write python code to answer user requests. You have access to {{ SCHEMA }} and {{ TOOLS }}."""
185
185
  for event in run(
186
186
  session=session,
187
187
  messages=messages,
@@ -195,7 +195,7 @@ for event in run(
195
195
  The emitted events are the same as in the minimal example; only the `SystemInstructionEvent` content changes to reflect your custom template:
196
196
 
197
197
  ```text
198
- SystemInstructionEvent(content='Write python code to answer user requests. You have access to <schema...>, <symbols...> and <tools...>')
198
+ SystemInstructionEvent(content='Write python code to answer user requests. You have access to <schema...> and <tools...>')
199
199
  ```
200
200
 
201
201
  </details>
@@ -164,10 +164,10 @@ MessageEvent(message=UserMessage(content="Code rejected: Forbidden import: os"))
164
164
  <details>
165
165
  <summary>Custom system prompt</summary>
166
166
  You can pass a Jinja template to override the default one (see `src/llmalchemy/prompt.jinja`).
167
- It is recommended that the template contains vars {{ SCHEMA }}, {{ SYMBOLS }}, {{ TOOLS }} and {{ IMPORTS }}.
167
+ It is recommended that the template contains vars {{ SCHEMA }}, {{ TOOLS }} and {{ IMPORTS }}.
168
168
 
169
169
  ```python
170
- prompt = """Write python code to answer user requests. You have access to {{ SCHEMA }}, {{ SYMBOLS }} and {{ TOOLS }}."""
170
+ prompt = """Write python code to answer user requests. You have access to {{ SCHEMA }} and {{ TOOLS }}."""
171
171
  for event in run(
172
172
  session=session,
173
173
  messages=messages,
@@ -181,7 +181,7 @@ for event in run(
181
181
  The emitted events are the same as in the minimal example; only the `SystemInstructionEvent` content changes to reflect your custom template:
182
182
 
183
183
  ```text
184
- SystemInstructionEvent(content='Write python code to answer user requests. You have access to <schema...>, <symbols...> and <tools...>')
184
+ SystemInstructionEvent(content='Write python code to answer user requests. You have access to <schema...> and <tools...>')
185
185
  ```
186
186
 
187
187
  </details>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llmalchemy"
3
- version = "2.0.0"
3
+ version = "3.0.0"
4
4
  description = "A full-access agent pattern."
5
5
  requires-python = ">=3.13"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llmalchemy"
3
- version = "2.0.0"
3
+ version = "3.0.0"
4
4
  description = "A full-access agent pattern."
5
5
  requires-python = ">=3.13"
6
6
  license = { text = "MIT" }
@@ -145,51 +145,28 @@ def _init_namespace(
145
145
  session: Session,
146
146
  base: type[DeclarativeBase],
147
147
  tools: list[Tool],
148
- ) -> tuple[dict, dict[str, str]]:
148
+ ) -> dict:
149
149
  """Build a fresh code execution namespace for one run.
150
150
 
151
151
  Injects ``session``, ORM model classes, association tables, tool functions,
152
152
  and ``disclose``.
153
-
154
- Returns:
155
- The namespace, and a ``{name: description}`` dict of every injected
156
- **infrastructure** symbol. Tool symbols are excluded — their source of
157
- truth is the ``Tool`` object itself, rendered separately by
158
- ``_render_tools_summary``.
159
153
  """
160
154
  namespace: dict = {}
161
- descriptions: dict[str, str] = {}
162
155
 
163
156
  namespace["session"] = session
164
- descriptions["session"] = "a `sqlalchemy.orm.Session` connected to the database."
165
157
 
166
- orm_classes = mapped_classes(base)
167
- for cls in orm_classes:
158
+ for cls in mapped_classes(base):
168
159
  namespace[cls.__name__] = cls
169
- if orm_classes:
170
- names = ", ".join(cls.__name__ for cls in orm_classes)
171
- descriptions[names] = "ORM model classes (see schema above)."
172
-
173
- tables = association_tables(base)
174
- namespace.update(tables)
175
- if tables:
176
- names = ", ".join(tables)
177
- descriptions[names] = (
178
- "`sqlalchemy.Table` association objects for many-to-many junctions,"
179
- " usable directly (e.g. `session.execute(insert(table), rows)`)."
180
- )
160
+
161
+ namespace.update(association_tables(base))
181
162
 
182
163
  for t in tools:
183
164
  namespace[t.name] = t.fn
184
165
 
185
166
  if tools:
186
167
  namespace["disclose"] = make_disclose_fn(tools)
187
- descriptions["disclose"] = (
188
- "`disclose(name: str) -> str` — prints the full signature"
189
- " and docstring of a tool. Call it before using a tool you haven't seen yet."
190
- )
191
168
 
192
- return namespace, descriptions
169
+ return namespace
193
170
 
194
171
 
195
172
  def run(
@@ -202,6 +179,7 @@ def run(
202
179
  allowed_imports: list[str] | None = None,
203
180
  prompt_template: str | Path | None = None,
204
181
  output_extensions: type[BaseModel] | None = None,
182
+ referenceable_tables: list[str] | None = None,
205
183
  ) -> Iterator[Event]:
206
184
  """Execute the agentic loop.
207
185
 
@@ -219,10 +197,12 @@ def run(
219
197
  output_extensions: Optional Pydantic model to force in the LM structured output.
220
198
  thinking_effort: Level of thinking for provider-native reasoning tokens.
221
199
  prompt_template: Custom jinja system prompt. Should contain placeholders for:
222
- - ``SCHEMA``: used to show agent the source code of ORM classes
223
- - ``SYMBOLS``: used to show ageent all pre-loaded namespace symbols.
224
- - ``TOOLS``: usedf to show the agent tool names + short descriptions.
200
+ - ``SCHEMA``: used to show the agent the source code of the schema module.
201
+ - ``TOOLS``: used to show the agent tool names + short descriptions.
225
202
  - ``IMPORTS``: used to show the agent which modules it may import.
203
+ referenceable_tables: Schema table names the agent may cite inline in its
204
+ messages, e.g. ``[authors:1]``. Defaults to ``None`` (disabled);
205
+ unknown names raise ``ValueError``.
226
206
 
227
207
  Yields:
228
208
  ``Event``: system instruction, loop signals, and conversation messages.
@@ -238,6 +218,7 @@ def run(
238
218
  allowed_imports,
239
219
  prompt_template,
240
220
  output_extensions,
221
+ referenceable_tables,
241
222
  )
242
223
 
243
224
 
@@ -251,14 +232,15 @@ def _run(
251
232
  allowed_imports: list[str] | None,
252
233
  prompt_template: str | Path | None,
253
234
  output_extensions: type[BaseModel] | None,
235
+ referenceable_tables: list[str] | None = None,
254
236
  ) -> Iterator[Event]:
255
237
  """Agentic loop body (see :func:`run`)."""
256
238
  # Initialize everything
257
239
  tools = tools or []
258
240
  allowed_imports = ["sqlalchemy"] if allowed_imports is None else allowed_imports
259
241
  output_schema = _build_output_schema(output_extensions)
260
- namespace, descriptions = _init_namespace(session, base, tools)
261
- system_instruction = render(base, tools, descriptions, prompt_template, allowed_imports)
242
+ namespace = _init_namespace(session, base, tools)
243
+ system_instruction = render(base, tools, prompt_template, allowed_imports, referenceable_tables)
262
244
  yield SystemInstructionEvent(system_instruction)
263
245
  before = serialize(session, base)
264
246
 
@@ -268,6 +250,8 @@ def _run(
268
250
  )
269
251
  code = output.code
270
252
 
253
+ # Note: agent can inline cite tables, we should verify that they are valid (issue 30)
254
+
271
255
  # Loop until model is over with the task
272
256
  loops = 0
273
257
  while code:
@@ -2,18 +2,18 @@
2
2
 
3
3
  import inspect
4
4
  import re
5
+ import sys
5
6
  import warnings
6
7
  from pathlib import Path
7
8
 
8
9
  from lmdk import render_template
9
10
  from sqlalchemy.orm import DeclarativeBase
10
11
 
11
- from .database import mapped_classes
12
12
  from .tools import Tool
13
13
 
14
14
  _TEMPLATE_PATH = Path(__file__).parent / "prompt.jinja"
15
15
 
16
- _REQUIRED_MARKERS = ("SCHEMA", "SYMBOLS", "TOOLS", "IMPORTS")
16
+ _REQUIRED_MARKERS = ("SCHEMA", "TOOLS", "IMPORTS")
17
17
 
18
18
 
19
19
  class LLMAlchemyPromptWarning(UserWarning):
@@ -21,19 +21,12 @@ class LLMAlchemyPromptWarning(UserWarning):
21
21
 
22
22
 
23
23
  def _render_schema_source(base: type[DeclarativeBase]) -> str:
24
- """Return the source code of ORM classes for the LM prompt.
24
+ """Return the source of the module defining *base* for the LM prompt.
25
25
 
26
- Extracts source code via ``inspect.getsource`` for every mapped class
27
- registered under *base*. The sources are concatenated separated by
28
- blank lines.
26
+ The whole module is shown (enums, junction tables, mixins, constraints),
27
+ so keep implementation details such as ORM hooks in a separate module.
29
28
  """
30
- sources = [inspect.getsource(cls) for cls in mapped_classes(base)]
31
- return "\n\n".join(sources)
32
-
33
-
34
- def _render_symbols(descriptions: dict[str, str]) -> str:
35
- """Format the symbol descriptions dict as a Markdown bullet list."""
36
- return "\n".join(f"- `{name}`: {desc}" for name, desc in descriptions.items())
29
+ return inspect.getsource(sys.modules[base.__module__])
37
30
 
38
31
 
39
32
  def _render_tools_summary(tools: list[Tool]) -> str:
@@ -46,9 +39,34 @@ def _render_tools_summary(tools: list[Tool]) -> str:
46
39
  def _render_imports(allowed_imports: list[str]) -> str:
47
40
  """Render the import policy as one sentence."""
48
41
  if not allowed_imports:
49
- return "`import` statements are forbidden. Use only the pre-loaded symbols listed below."
42
+ return (
43
+ "`import` statements are forbidden. Use only the symbols pre-loaded in your namespace."
44
+ )
50
45
  modules = ", ".join(f"`{m}`" for m in allowed_imports)
51
- return f"Only these modules may be imported: {modules}."
46
+ return f"Module imports are forbidden, with exception of: {modules}."
47
+
48
+
49
+ def _render_references(base: type[DeclarativeBase], referenceable_tables: list[str] | None) -> str:
50
+ """Render the row-reference instruction for the allowed tables.
51
+
52
+ Table names are validated against the schema so a typo raises instead of
53
+ silently letting the agent cite a table that does not exist.
54
+ """
55
+ if not referenceable_tables:
56
+ return ""
57
+ known = base.metadata.tables
58
+ unknown = [name for name in referenceable_tables if name not in known]
59
+ if unknown:
60
+ raise ValueError(
61
+ f"Unknown referenceable table(s): {', '.join(unknown)}. "
62
+ f"Available tables: {', '.join(sorted(known))}."
63
+ )
64
+ names = ", ".join(f"`{name}`" for name in referenceable_tables)
65
+ return (
66
+ f"You can reference specific rows in {names} by inlining "
67
+ f"`[{referenceable_tables[0]}:row_pk]` in your message to the user, "
68
+ "where `row_pk` is the row's primary key value."
69
+ )
52
70
 
53
71
 
54
72
  def _check_markers(source: str) -> None:
@@ -66,39 +84,32 @@ def _check_markers(source: str) -> None:
66
84
  def render(
67
85
  base: type[DeclarativeBase],
68
86
  tools: list[Tool],
69
- descriptions: dict[str, str],
70
87
  template: str | Path | None = None,
71
88
  allowed_imports: list[str] | None = None,
89
+ referenceable_tables: list[str] | None = None,
72
90
  ) -> str:
73
91
  """Build the system instruction for the LM with all context parts.
74
92
 
75
93
  Args:
76
94
  base: The declarative base describing the schema.
77
95
  tools: User-provided tools registered for this run.
78
- descriptions: ``{name: description}`` of every namespace symbol,
79
- as returned by ``_init_namespace`` in ``agent.py``.
80
96
  template: A Jinja template source string, a ``Path`` to a template
81
97
  file, or ``None`` to use the shipped default.
82
98
  allowed_imports: Modules the agent may import. ``None`` or an empty
83
99
  list means imports are forbidden.
100
+ referenceable_tables: Schema table names the agent may cite inline,
101
+ e.g. ``[authors:1]``. ``None`` means citations are disabled.
84
102
  """
85
103
  if template is None:
86
104
  path: Path = _TEMPLATE_PATH
87
105
  source = path.read_text()
88
- _check_markers(source)
89
- return render_template(
90
- template=source,
91
- SCHEMA=_render_schema_source(base=base),
92
- SYMBOLS=_render_symbols(descriptions),
93
- TOOLS=_render_tools_summary(tools),
94
- IMPORTS=_render_imports(allowed_imports or []),
95
- )
96
- source = template.read_text() if isinstance(template, Path) else template
106
+ else:
107
+ source = template.read_text() if isinstance(template, Path) else template
97
108
  _check_markers(source)
98
109
  return render_template(
99
110
  template=source,
100
111
  SCHEMA=_render_schema_source(base=base),
101
- SYMBOLS=_render_symbols(descriptions),
102
112
  TOOLS=_render_tools_summary(tools),
103
113
  IMPORTS=_render_imports(allowed_imports or []),
114
+ REFERENCES=_render_references(base, referenceable_tables),
104
115
  )
@@ -0,0 +1,25 @@
1
+ You are an expert assistant with access to a relational database containing the user's project.
2
+ You can interact with the database using Python code and SQLAlchemy.
3
+ Your task is to navigate and edit the project to fulfill the user's requests.
4
+
5
+ # Guidelines
6
+ - **Code Execution**: Run Python code to query or modify the database. Use `print()` to inspect results.
7
+ - **Iteration**: Use the execution output (sent as an automatic user message) to decide if you need to run more code or provide a final answer to end the turn.
8
+ - **Efficiency**: Keep `print()` statements concise, avoid redundant imports or unnecessary checks.
9
+ - **Persistence**: the Python namespace (variables, imports, ...) carry over between cells only within the same turn. A new user message means a newly initialized namespace.
10
+ - **Imports**: {{ IMPORTS }}
11
+ {% if REFERENCES %}- **References**: {{ REFERENCES }}{% endif %}
12
+ {% if TOOLS %}- **Tools**: Several tools (pre-built python functions) are available in your namespace. Use `print(disclose(name))` to discover a tool's full signature and docstring before calling it.{% endif %}
13
+
14
+ # Database Schema
15
+ ```python
16
+ {{ SCHEMA }}
17
+ ```
18
+
19
+ Your namespace is pre-loaded with `session` and every class and association table shown above — no need to import or redefine them.
20
+
21
+ {% if TOOLS %}
22
+ # Available Tools
23
+ The tools below are also pre-loaded in the namespace.
24
+ {{ TOOLS }}
25
+ {% endif %}
@@ -1,27 +0,0 @@
1
- You are an expert assistant with access to a relational database containing the user's project.
2
- You can interact with the database using Python code and SQLAlchemy.
3
-
4
- # Guidelines
5
- - **Code Execution**: Run Python code to query or modify the database. Use `print()` to inspect results.
6
- - **Imports**: {{ IMPORTS }}
7
- - **Persistence**: Variables (and any allowed imports) carry over between cells, until the user sends a new message.
8
- - **Printing**: Keep `print()` statements concise, but print anything you may need in a later turn (ids, key values, short findings).
9
- - **Efficiency**: Avoid redundant imports or session initialization.
10
- - **Iteration**: Use the execution output (sent as an automatic user message) to decide if you need to run more code or provide a final answer.
11
- {% if TOOLS %}- **Tools**: Several tools are available in your namespace. Use `print(disclose(name))` to discover a tool's full signature and docstring before calling it.{% endif %}
12
- - **Transactions**: the SQLAlchemy `session` is also shared by every cell, nothing is committed or rolled back for you.
13
- - Uncommitted work piles up across cells.
14
- - A code execution that raises halfway leaves the part that already ran pending.
15
-
16
- # Database Schema
17
- ```python
18
- {{ SCHEMA }}
19
- ```
20
-
21
- # Available Symbols
22
- The following symbols are pre-loaded in your namespace:
23
- {{ SYMBOLS }}
24
- {% if TOOLS %}
25
- # Available Tools
26
- {{ TOOLS }}
27
- {% endif %}