llmalchemy 2.0.0__tar.gz → 2.1.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: 2.1.0
4
4
  Summary: A full-access agent pattern.
5
5
  Author: Ignacio Llorca
6
6
  Author-email: Ignacio Llorca <nllorca@proton.me>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llmalchemy"
3
- version = "2.0.0"
3
+ version = "2.1.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 = "2.1.0"
4
4
  description = "A full-access agent pattern."
5
5
  requires-python = ">=3.13"
6
6
  license = { text = "MIT" }
@@ -202,6 +202,7 @@ def run(
202
202
  allowed_imports: list[str] | None = None,
203
203
  prompt_template: str | Path | None = None,
204
204
  output_extensions: type[BaseModel] | None = None,
205
+ referenceable_tables: list[str] | None = None,
205
206
  ) -> Iterator[Event]:
206
207
  """Execute the agentic loop.
207
208
 
@@ -223,6 +224,9 @@ def run(
223
224
  - ``SYMBOLS``: used to show ageent all pre-loaded namespace symbols.
224
225
  - ``TOOLS``: usedf to show the agent tool names + short descriptions.
225
226
  - ``IMPORTS``: used to show the agent which modules it may import.
227
+ referenceable_tables: Schema table names the agent may cite inline in its
228
+ messages, e.g. ``[authors:1]``. Defaults to ``None`` (disabled);
229
+ unknown names raise ``ValueError``.
226
230
 
227
231
  Yields:
228
232
  ``Event``: system instruction, loop signals, and conversation messages.
@@ -238,6 +242,7 @@ def run(
238
242
  allowed_imports,
239
243
  prompt_template,
240
244
  output_extensions,
245
+ referenceable_tables,
241
246
  )
242
247
 
243
248
 
@@ -251,6 +256,7 @@ def _run(
251
256
  allowed_imports: list[str] | None,
252
257
  prompt_template: str | Path | None,
253
258
  output_extensions: type[BaseModel] | None,
259
+ referenceable_tables: list[str] | None = None,
254
260
  ) -> Iterator[Event]:
255
261
  """Agentic loop body (see :func:`run`)."""
256
262
  # Initialize everything
@@ -258,7 +264,9 @@ def _run(
258
264
  allowed_imports = ["sqlalchemy"] if allowed_imports is None else allowed_imports
259
265
  output_schema = _build_output_schema(output_extensions)
260
266
  namespace, descriptions = _init_namespace(session, base, tools)
261
- system_instruction = render(base, tools, descriptions, prompt_template, allowed_imports)
267
+ system_instruction = render(
268
+ base, tools, descriptions, prompt_template, allowed_imports, referenceable_tables
269
+ )
262
270
  yield SystemInstructionEvent(system_instruction)
263
271
  before = serialize(session, base)
264
272
 
@@ -268,6 +276,8 @@ def _run(
268
276
  )
269
277
  code = output.code
270
278
 
279
+ # Note: agent can inline cite tables, we should verify that they are valid (issue 30)
280
+
271
281
  # Loop until model is over with the task
272
282
  loops = 0
273
283
  while code:
@@ -51,6 +51,29 @@ def _render_imports(allowed_imports: list[str]) -> str:
51
51
  return f"Only these modules may be imported: {modules}."
52
52
 
53
53
 
54
+ def _render_references(base: type[DeclarativeBase], referenceable_tables: list[str] | None) -> str:
55
+ """Render the row-reference instruction for the allowed tables.
56
+
57
+ Table names are validated against the schema so a typo raises instead of
58
+ silently letting the agent cite a table that does not exist.
59
+ """
60
+ if not referenceable_tables:
61
+ return ""
62
+ known = base.metadata.tables
63
+ unknown = [name for name in referenceable_tables if name not in known]
64
+ if unknown:
65
+ raise ValueError(
66
+ f"Unknown referenceable table(s): {', '.join(unknown)}. "
67
+ f"Available tables: {', '.join(sorted(known))}."
68
+ )
69
+ names = ", ".join(f"`{name}`" for name in referenceable_tables)
70
+ return (
71
+ f"You can reference specific rows in {names} by inlining "
72
+ f"`[{referenceable_tables[0]}:row_pk]` in your message to the user, "
73
+ "where `row_pk` is the row's primary key value."
74
+ )
75
+
76
+
54
77
  def _check_markers(source: str) -> None:
55
78
  """Warn for each required Jinja variable missing from the raw template source."""
56
79
  for marker in _REQUIRED_MARKERS:
@@ -69,6 +92,7 @@ def render(
69
92
  descriptions: dict[str, str],
70
93
  template: str | Path | None = None,
71
94
  allowed_imports: list[str] | None = None,
95
+ referenceable_tables: list[str] | None = None,
72
96
  ) -> str:
73
97
  """Build the system instruction for the LM with all context parts.
74
98
 
@@ -81,19 +105,14 @@ def render(
81
105
  file, or ``None`` to use the shipped default.
82
106
  allowed_imports: Modules the agent may import. ``None`` or an empty
83
107
  list means imports are forbidden.
108
+ referenceable_tables: Schema table names the agent may cite inline,
109
+ e.g. ``[authors:1]``. ``None`` means citations are disabled.
84
110
  """
85
111
  if template is None:
86
112
  path: Path = _TEMPLATE_PATH
87
113
  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
114
+ else:
115
+ source = template.read_text() if isinstance(template, Path) else template
97
116
  _check_markers(source)
98
117
  return render_template(
99
118
  template=source,
@@ -101,4 +120,5 @@ def render(
101
120
  SYMBOLS=_render_symbols(descriptions),
102
121
  TOOLS=_render_tools_summary(tools),
103
122
  IMPORTS=_render_imports(allowed_imports or []),
123
+ REFERENCES=_render_references(base, referenceable_tables),
104
124
  )
@@ -4,10 +4,11 @@ You can interact with the database using Python code and SQLAlchemy.
4
4
  # Guidelines
5
5
  - **Code Execution**: Run Python code to query or modify the database. Use `print()` to inspect results.
6
6
  - **Imports**: {{ IMPORTS }}
7
- - **Persistence**: Variables (and any allowed imports) carry over between cells, until the user sends a new message.
8
7
  - **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.
8
+ {% if REFERENCES %}- **References**: {{ REFERENCES }}{% endif %}
9
+ - **Efficiency**: Avoid redundant imports, session initialization and repeated prints.
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 to end the turn.
11
+ - **Persistence**: the Python namespace (variables, imports, ...) carry over between cells only within the same turn. A new user message means a newly initialized namespace.
11
12
  {% 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
13
  - **Transactions**: the SQLAlchemy `session` is also shared by every cell, nothing is committed or rolled back for you.
13
14
  - Uncommitted work piles up across cells.
File without changes