Merge pull request #151 from ggozad/fix/graph-config

Allow custom config when building research/deep ask graphs.
This commit is contained in:
Yiorgis Gozadinos 2025-11-21 12:45:07 +02:00 committed by GitHub
commit a4b82982c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 33 additions and 13 deletions

View file

@ -4,6 +4,10 @@
### Fixed ### Fixed
- **AG-UI Activity Events**: Activity events now correctly use structured dict content instead of strings - **AG-UI Activity Events**: Activity events now correctly use structured dict content instead of strings
- **Graph Configuration**: Graph builder functions now properly accept and use non-global config (#149)
- `build_research_graph()` and `build_deep_qa_graph()` now pass config to all agents and model creation
- `get_model()` utility function accepts `config` parameter (defaults to global Config)
- Allows creating multiple graphs with different configurations in the same application
## [0.17.2] - 2025-11-19 ## [0.17.2] - 2025-11-19

View file

@ -10,6 +10,8 @@ from pydantic_ai.output import ToolOutput
from pydantic_graph.beta import StepContext from pydantic_graph.beta import StepContext
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
from haiku.rag.config import Config
from haiku.rag.config.models import AppConfig
from haiku.rag.graph.agui.emitter import AGUIEmitter from haiku.rag.graph.agui.emitter import AGUIEmitter
from haiku.rag.graph.common import get_model from haiku.rag.graph.common import get_model
from haiku.rag.graph.common.models import ResearchPlan, SearchAnswer from haiku.rag.graph.common.models import ResearchPlan, SearchAnswer
@ -55,6 +57,7 @@ def create_plan_node[AgentDepsT: GraphAgentDeps](
deps_type: type[AgentDepsT], deps_type: type[AgentDepsT],
activity_message: str = "Creating plan", activity_message: str = "Creating plan",
output_retries: int | None = None, output_retries: int | None = None,
config: AppConfig = Config,
) -> Callable[[StepContext[Any, Any, None]], Awaitable[None]]: ) -> Callable[[StepContext[Any, Any, None]], Awaitable[None]]:
"""Create a plan node for any graph. """Create a plan node for any graph.
@ -64,6 +67,7 @@ def create_plan_node[AgentDepsT: GraphAgentDeps](
deps_type: Type of dependencies for the agent (e.g., ResearchDependencies, DeepQADependencies) deps_type: Type of dependencies for the agent (e.g., ResearchDependencies, DeepQADependencies)
activity_message: Message to show during planning activity activity_message: Message to show during planning activity
output_retries: Number of output retries for the agent (optional) output_retries: Number of output retries for the agent (optional)
config: AppConfig object (defaults to global Config)
Returns: Returns:
Async function that can be used as a graph step Async function that can be used as a graph step
@ -80,7 +84,7 @@ def create_plan_node[AgentDepsT: GraphAgentDeps](
try: try:
# Build agent configuration # Build agent configuration
agent_config = { agent_config = {
"model": get_model(provider, model), "model": get_model(provider, model, config),
"output_type": ResearchPlan, "output_type": ResearchPlan,
"instructions": ( "instructions": (
PLAN_PROMPT PLAN_PROMPT
@ -136,6 +140,7 @@ def create_search_node[AgentDepsT: GraphAgentDeps](
with_step_wrapper: bool = True, with_step_wrapper: bool = True,
success_message_format: str = "Answered: {sub_q}", success_message_format: str = "Answered: {sub_q}",
handle_exceptions: bool = False, handle_exceptions: bool = False,
config: AppConfig = Config,
) -> Callable[[StepContext[Any, Any, str]], Awaitable[SearchAnswer]]: ) -> Callable[[StepContext[Any, Any, str]], Awaitable[SearchAnswer]]:
"""Create a search_one node for any graph. """Create a search_one node for any graph.
@ -146,6 +151,7 @@ def create_search_node[AgentDepsT: GraphAgentDeps](
with_step_wrapper: Whether to wrap with agui_emitter start/finish step with_step_wrapper: Whether to wrap with agui_emitter start/finish step
success_message_format: Format string for success activity message success_message_format: Format string for success activity message
handle_exceptions: Whether to handle exceptions with fallback answer handle_exceptions: Whether to handle exceptions with fallback answer
config: AppConfig object (defaults to global Config)
Returns: Returns:
Async function that can be used as a graph step Async function that can be used as a graph step
@ -178,6 +184,7 @@ def create_search_node[AgentDepsT: GraphAgentDeps](
deps_type, deps_type,
success_message_format, success_message_format,
handle_exceptions, handle_exceptions,
config,
) )
finally: finally:
if deps.agui_emitter and with_step_wrapper: if deps.agui_emitter and with_step_wrapper:
@ -195,6 +202,7 @@ async def _do_search[AgentDepsT: GraphAgentDeps](
deps_type: type[AgentDepsT], deps_type: type[AgentDepsT],
success_message_format: str, success_message_format: str,
handle_exceptions: bool, handle_exceptions: bool,
config: AppConfig,
) -> SearchAnswer: ) -> SearchAnswer:
"""Internal search implementation.""" """Internal search implementation."""
if deps.agui_emitter: if deps.agui_emitter:
@ -203,7 +211,7 @@ async def _do_search[AgentDepsT: GraphAgentDeps](
) )
agent = Agent( agent = Agent(
model=get_model(provider, model), model=get_model(provider, model, config),
output_type=ToolOutput(SearchAnswer, max_retries=3), output_type=ToolOutput(SearchAnswer, max_retries=3),
instructions=SEARCH_AGENT_PROMPT, instructions=SEARCH_AGENT_PROMPT,
retries=3, retries=3,

View file

@ -5,15 +5,19 @@ from pydantic_ai.providers.ollama import OllamaProvider
from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.providers.openai import OpenAIProvider
from haiku.rag.config import Config from haiku.rag.config import Config
from haiku.rag.config.models import AppConfig
def get_model(provider: str, model: str) -> OpenAIChatModel | str: def get_model(
provider: str, model: str, config: AppConfig = Config
) -> OpenAIChatModel | str:
""" """
Get a model instance for the specified provider and model name. Get a model instance for the specified provider and model name.
Args: Args:
provider: The model provider ("ollama", "vllm", or other) provider: The model provider ("ollama", "vllm", or other)
model: The model name model: The model name
config: AppConfig object (defaults to global Config)
Returns: Returns:
A configured model instance A configured model instance
@ -24,13 +28,13 @@ def get_model(provider: str, model: str) -> OpenAIChatModel | str:
if provider == "ollama": if provider == "ollama":
return OpenAIChatModel( return OpenAIChatModel(
model_name=model, model_name=model,
provider=OllamaProvider(base_url=f"{Config.providers.ollama.base_url}/v1"), provider=OllamaProvider(base_url=f"{config.providers.ollama.base_url}/v1"),
) )
elif provider == "vllm": elif provider == "vllm":
return OpenAIChatModel( return OpenAIChatModel(
model_name=model, model_name=model,
provider=OpenAIProvider( provider=OpenAIProvider(
base_url=f"{Config.providers.vllm.research_base_url or Config.providers.vllm.qa_base_url}/v1", base_url=f"{config.providers.vllm.research_base_url or config.providers.vllm.qa_base_url}/v1",
api_key="none", api_key="none",
), ),
) )

View file

@ -45,6 +45,7 @@ def build_deep_qa_graph(
deps_type=DeepQADependencies, # type: ignore[arg-type] deps_type=DeepQADependencies, # type: ignore[arg-type]
activity_message="Planning approach", activity_message="Planning approach",
output_retries=None, # Deep QA doesn't use output_retries output_retries=None, # Deep QA doesn't use output_retries
config=config,
) )
) # type: ignore[arg-type] ) # type: ignore[arg-type]
@ -57,6 +58,7 @@ def build_deep_qa_graph(
with_step_wrapper=False, # Deep QA doesn't wrap with agui_emitter step with_step_wrapper=False, # Deep QA doesn't wrap with agui_emitter step
success_message_format="Answered: {sub_q}", success_message_format="Answered: {sub_q}",
handle_exceptions=True, handle_exceptions=True,
config=config,
) )
) # type: ignore[arg-type] ) # type: ignore[arg-type]
@ -90,7 +92,7 @@ def build_deep_qa_graph(
try: try:
agent = Agent( agent = Agent(
model=get_model(provider, model), model=get_model(provider, model, config),
output_type=DeepQAEvaluation, output_type=DeepQAEvaluation,
instructions=DECISION_PROMPT, instructions=DECISION_PROMPT,
retries=3, retries=3,
@ -168,7 +170,7 @@ def build_deep_qa_graph(
) )
agent = Agent( agent = Agent(
model=get_model(provider, model), model=get_model(provider, model, config),
output_type=DeepQAAnswer, output_type=DeepQAAnswer,
instructions=prompt_template, instructions=prompt_template,
retries=3, retries=3,

View file

@ -52,6 +52,7 @@ def build_research_graph(
deps_type=ResearchDependencies, # type: ignore[arg-type] deps_type=ResearchDependencies, # type: ignore[arg-type]
activity_message="Creating research plan", activity_message="Creating research plan",
output_retries=3, output_retries=3,
config=config,
) )
) # type: ignore[arg-type] ) # type: ignore[arg-type]
@ -64,6 +65,7 @@ def build_research_graph(
with_step_wrapper=True, with_step_wrapper=True,
success_message_format="Found answer with {confidence:.0%} confidence", success_message_format="Found answer with {confidence:.0%} confidence",
handle_exceptions=True, handle_exceptions=True,
config=config,
) )
) # type: ignore[arg-type] ) # type: ignore[arg-type]
@ -97,7 +99,7 @@ def build_research_graph(
try: try:
agent = Agent( agent = Agent(
model=get_model(provider, model), model=get_model(provider, model, config),
output_type=InsightAnalysis, output_type=InsightAnalysis,
instructions=INSIGHT_AGENT_PROMPT, instructions=INSIGHT_AGENT_PROMPT,
retries=3, retries=3,
@ -155,7 +157,7 @@ def build_research_graph(
try: try:
agent = Agent( agent = Agent(
model=get_model(provider, model), model=get_model(provider, model, config),
output_type=EvaluationResult, output_type=EvaluationResult,
instructions=DECISION_AGENT_PROMPT, instructions=DECISION_AGENT_PROMPT,
retries=3, retries=3,
@ -231,7 +233,7 @@ def build_research_graph(
try: try:
agent = Agent( agent = Agent(
model=get_model(provider, model), model=get_model(provider, model, config),
output_type=ResearchReport, output_type=ResearchReport,
instructions=SYNTHESIS_AGENT_PROMPT, instructions=SYNTHESIS_AGENT_PROMPT,
retries=3, retries=3,

View file

@ -13,7 +13,7 @@ async def test_deep_qa_graph_end_to_end(monkeypatch, temp_db_path):
"""Test deep Q&A graph with mocked LLM using TestModel.""" """Test deep Q&A graph with mocked LLM using TestModel."""
# Mock get_model to return TestModel which generates valid schema-compliant data # Mock get_model to return TestModel which generates valid schema-compliant data
def test_model_factory(provider, model): def test_model_factory(provider, model, config=None):
return TestModel() return TestModel()
monkeypatch.setattr("haiku.rag.graph.common.utils.get_model", test_model_factory) monkeypatch.setattr("haiku.rag.graph.common.utils.get_model", test_model_factory)
@ -47,7 +47,7 @@ async def test_deep_qa_with_citations(monkeypatch, temp_db_path):
"""Test deep Q&A with citations enabled using TestModel.""" """Test deep Q&A with citations enabled using TestModel."""
# Mock get_model to return TestModel # Mock get_model to return TestModel
def test_model_factory(provider, model): def test_model_factory(provider, model, config=None):
return TestModel() return TestModel()
monkeypatch.setattr("haiku.rag.graph.common.utils.get_model", test_model_factory) monkeypatch.setattr("haiku.rag.graph.common.utils.get_model", test_model_factory)

View file

@ -36,7 +36,7 @@ async def test_graph_end_to_end_with_test_model(monkeypatch, temp_db_path):
"""Test research graph with mocked LLM using AG-UI events.""" """Test research graph with mocked LLM using AG-UI events."""
# Mock get_model to return TestModel which generates valid schema-compliant data # Mock get_model to return TestModel which generates valid schema-compliant data
def test_model_factory(_provider, _model): def test_model_factory(_provider, _model, _config=None):
return TestModel() return TestModel()
monkeypatch.setattr("haiku.rag.graph.common.utils.get_model", test_model_factory) monkeypatch.setattr("haiku.rag.graph.common.utils.get_model", test_model_factory)