Skip to content

Commit 67ac756

Browse files
v23.3.0 (#113)
* add structured outputs * fix audio stripping bug * make fix and update readme/docs * update deps
1 parent e81b9fa commit 67ac756

File tree

12 files changed

+323
-188
lines changed

12 files changed

+323
-188
lines changed

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Build your AI agents in three lines of code!
2929
* Extensible Tooling
3030
* Automatic Tool Workflows
3131
* Autonomous Operation
32+
* Structured Outputs
3233
* Knowledge Base
3334
* MCP Support
3435
* Guardrails
@@ -60,6 +61,7 @@ Build your AI agents in three lines of code!
6061
* Input and output guardrails for content filtering, safety, and data sanitization
6162
* Generate custom images based on text prompts with storage on S3 compatible services
6263
* Automatic sequential tool workflows allowing agents to chain multiple tools
64+
* Deterministically return structured outputs
6365
* Combine with event-driven systems to create autonomous agents
6466

6567
## Stack
@@ -306,6 +308,38 @@ async for response in solana_agent.process("user123", "What is in this image? De
306308
print(response, end="")
307309
```
308310

311+
### Structured Outputs
312+
313+
```python
314+
from solana_agent import SolanaAgent
315+
316+
config = {
317+
"openai": {
318+
"api_key": "your-openai-api-key",
319+
},
320+
"agents": [
321+
{
322+
"name": "researcher",
323+
"instructions": "You are a research expert.",
324+
"specialization": "Researcher",
325+
}
326+
],
327+
}
328+
329+
solana_agent = SolanaAgent(config=config)
330+
331+
class ResearchProposal(BaseModel):
332+
title: str
333+
abstract: str
334+
key_points: list[str]
335+
336+
full_response = None
337+
async for response in solana_agent.process("user123", "Research the life of Ben Franklin - the founding Father.", output_model=ResearchProposal):
338+
full_response = response
339+
340+
print(full_response.model_dump())
341+
```
342+
309343
### Command Line Interface (CLI)
310344

311345
Solana Agent includes a command-line interface (CLI) for text-based chat using a configuration file.
@@ -540,6 +574,8 @@ async for response in solana_agent.process("user123", "Summarize the annual repo
540574

541575
Guardrails allow you to process and potentially modify user input before it reaches the agent (Input Guardrails) and agent output before it's sent back to the user (Output Guardrails). This is useful for implementing safety checks, content moderation, data sanitization, or custom transformations.
542576

577+
Guardrails don't work with structured outputs.
578+
543579
Solana Agent provides a built-in PII scrubber based on [scrubadub](https://github.com/LeapBeyond/scrubadub).
544580

545581
```python
@@ -580,6 +616,8 @@ config = {
580616

581617
#### Example Custom Guardrails
582618

619+
Guardrails don't work with structured outputs.
620+
583621
```python
584622
from solana_agent import InputGuardrail, OutputGuardrail
585623
import logging

docs/index.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,40 @@ Image/Text Streaming
234234
print(response, end="")
235235
236236
237+
Structured Outputs
238+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
239+
240+
.. code-block:: python
241+
242+
from solana_agent import SolanaAgent
243+
244+
config = {
245+
"openai": {
246+
"api_key": "your-openai-api-key",
247+
},
248+
"agents": [
249+
{
250+
"name": "researcher",
251+
"instructions": "You are a research expert.",
252+
"specialization": "Researcher",
253+
}
254+
],
255+
}
256+
257+
solana_agent = SolanaAgent(config=config)
258+
259+
class ResearchProposal(BaseModel):
260+
title: str
261+
abstract: str
262+
key_points: list[str]
263+
264+
full_response = None
265+
async for response in solana_agent.process("user123", "Research the life of Ben Franklin - the founding Father.", output_model=ResearchProposal):
266+
full_response = response
267+
268+
print(full_response.model_dump())
269+
270+
237271
Command Line Interface (CLI)
238272
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
239273

@@ -474,6 +508,8 @@ Guardrails - Optional
474508

475509
Guardrails allow you to process and potentially modify user input before it reaches the agent (Input Guardrails) and agent output before it's sent back to the user (Output Guardrails). This is useful for implementing safety checks, content moderation, data sanitization, or custom transformations.
476510

511+
Guardrails don't apply to structured outputs.
512+
477513
Solana Agent provides a built-in PII scrubber based on scrubadub.
478514

479515
.. code-block:: python
@@ -515,6 +551,8 @@ Solana Agent provides a built-in PII scrubber based on scrubadub.
515551
Example Custom Guardrails - Optional
516552
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
517553

554+
Guardrails don't apply to structured outputs.
555+
518556
.. code-block:: python
519557
520558
from solana_agent import InputGuardrail, OutputGuardrail

poetry.lock

Lines changed: 161 additions & 143 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "solana-agent"
3-
version = "29.2.3"
3+
version = "29.3.0"
44
description = "AI Agents for Solana"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"
@@ -24,13 +24,13 @@ python_paths = [".", "tests"]
2424

2525
[tool.poetry.dependencies]
2626
python = ">=3.12,<4.0"
27-
openai = "1.82.0"
27+
openai = "1.82.1"
2828
pydantic = ">=2"
2929
pymongo = "4.13.0"
3030
zep-cloud = "2.12.3"
3131
instructor = "1.8.3"
32-
pinecone = "7.0.1"
33-
llama-index-core = "0.12.37"
32+
pinecone = "7.0.2"
33+
llama-index-core = "0.12.39"
3434
llama-index-embeddings-openai = "0.3.1"
3535
pypdf = "5.5.0"
3636
scrubadub = "2.0.1"

solana_agent/adapters/openai_adapter.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,8 @@ async def parse_structured_output(
410410
api_key: Optional[str] = None,
411411
base_url: Optional[str] = None,
412412
model: Optional[str] = None,
413+
functions: Optional[List[Dict[str, Any]]] = None,
414+
function_call: Optional[Union[str, Dict[str, Any]]] = None,
413415
) -> T: # pragma: no cover
414416
"""Generate structured output using Pydantic model parsing with Instructor."""
415417

@@ -431,13 +433,18 @@ async def parse_structured_output(
431433

432434
patched_client = instructor.from_openai(client, mode=Mode.TOOLS_STRICT)
433435

434-
# Use instructor's structured generation with function calling
435-
response = await patched_client.chat.completions.create(
436-
model=current_parse_model, # Use the determined model
437-
messages=messages,
438-
response_model=model_class,
439-
max_retries=2, # Automatically retry on validation errors
440-
)
436+
create_args = {
437+
"model": current_parse_model,
438+
"messages": messages,
439+
"response_model": model_class,
440+
"max_retries": 2, # Automatically retry on validation errors
441+
}
442+
if functions:
443+
create_args["tools"] = functions
444+
if function_call:
445+
create_args["function_call"] = function_call
446+
447+
response = await patched_client.chat.completions.create(**create_args)
441448
return response
442449
except Exception as e:
443450
logger.warning(

solana_agent/client/solana_agent.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
import json
99
import importlib.util
10-
from typing import AsyncGenerator, Dict, Any, List, Literal, Optional, Union
10+
from typing import AsyncGenerator, Dict, Any, List, Literal, Optional, Type, Union
11+
12+
from pydantic import BaseModel
1113

1214
from solana_agent.factories.agent_factory import SolanaAgentFactory
1315
from solana_agent.interfaces.client.client import SolanaAgent as SolanaAgentInterface
@@ -69,7 +71,8 @@ async def process(
6971
] = "mp4",
7072
router: Optional[RoutingInterface] = None,
7173
images: Optional[List[Union[str, bytes]]] = None,
72-
) -> AsyncGenerator[Union[str, bytes], None]: # pragma: no cover
74+
output_model: Optional[Type[BaseModel]] = None,
75+
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]: # pragma: no cover
7376
"""Process a user message (text or audio) and optional images, returning the response stream.
7477
7578
Args:
@@ -83,6 +86,7 @@ async def process(
8386
audio_input_format: Audio input format
8487
router: Optional routing service for processing
8588
images: Optional list of image URLs (str) or image bytes.
89+
output_model: Optional Pydantic model for structured output
8690
8791
Returns:
8892
Async generator yielding response chunks (text strings or audio bytes)
@@ -98,6 +102,7 @@ async def process(
98102
audio_input_format=audio_input_format,
99103
prompt=prompt,
100104
router=router,
105+
output_model=output_model,
101106
):
102107
yield chunk
103108

solana_agent/interfaces/client/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from abc import ABC, abstractmethod
2-
from typing import AsyncGenerator, Dict, Any, List, Literal, Optional, Union
2+
from typing import AsyncGenerator, Dict, Any, List, Literal, Optional, Type, Union
3+
4+
from pydantic import BaseModel
35
from solana_agent.interfaces.plugins.plugins import Tool
46
from solana_agent.interfaces.services.routing import RoutingService as RoutingInterface
57

@@ -35,7 +37,8 @@ async def process(
3537
] = "mp4",
3638
router: Optional[RoutingInterface] = None,
3739
images: Optional[List[Union[str, bytes]]] = None,
38-
) -> AsyncGenerator[Union[str, bytes], None]:
40+
output_model: Optional[Type[BaseModel]] = None,
41+
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]:
3942
"""Process a user message and return the response stream."""
4043
pass
4144

solana_agent/interfaces/providers/llm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ async def parse_structured_output(
4343
api_key: Optional[str] = None,
4444
base_url: Optional[str] = None,
4545
model: Optional[str] = None,
46+
functions: Optional[List[Dict[str, Any]]] = None,
47+
function_call: Optional[Union[str, Dict[str, Any]]] = None,
4648
) -> T:
4749
"""Generate structured output using a specific model class."""
4850
pass

solana_agent/interfaces/services/agent.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from abc import ABC, abstractmethod
2-
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union
2+
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Type, Union
3+
4+
from pydantic import BaseModel
35

46
from solana_agent.domains.agent import AIAgent
57

@@ -45,7 +47,8 @@ async def generate_response(
4547
] = "aac",
4648
prompt: Optional[str] = None,
4749
images: Optional[List[Union[str, bytes]]] = None,
48-
) -> AsyncGenerator[Union[str, bytes], None]:
50+
output_model: Optional[Type[BaseModel]] = None,
51+
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]:
4952
"""Generate a response from an agent."""
5053
pass
5154

solana_agent/interfaces/services/query.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from abc import ABC, abstractmethod
2-
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union
2+
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Type, Union
3+
4+
from pydantic import BaseModel
35

46
from solana_agent.interfaces.services.routing import RoutingService as RoutingInterface
57

@@ -35,7 +37,8 @@ async def process(
3537
prompt: Optional[str] = None,
3638
router: Optional[RoutingInterface] = None,
3739
images: Optional[List[Union[str, bytes]]] = None,
38-
) -> AsyncGenerator[Union[str, bytes], None]:
40+
output_model: Optional[Type[BaseModel]] = None,
41+
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]:
3942
"""Process the user request and generate a response."""
4043
pass
4144

0 commit comments

Comments
 (0)