Skip to content

Comments

UN-3276 support for user defined llm in hocon file with class key#270

Merged
Noravee merged 28 commits intomainfrom
UN-3276_Support_for_user-defined_llm_in_hocon_file_with_class_key
Jul 9, 2025
Merged

UN-3276 support for user defined llm in hocon file with class key#270
Noravee merged 28 commits intomainfrom
UN-3276_Support_for_user-defined_llm_in_hocon_file_with_class_key

Conversation

@Noravee
Copy link
Collaborator

@Noravee Noravee commented Jun 26, 2025

Changes

  • Add UserSpecifiedLangchainLlmFactory

Tests

  • Ran agent network with gpt-4.1-mini, which is not in the default llm info.

@Noravee Noravee marked this pull request as draft June 26, 2025 17:34
@Noravee Noravee requested review from d1donlydfink and ofrancon June 26, 2025 18:36
@Noravee Noravee marked this pull request as ready for review June 26, 2025 20:26
Copy link
Collaborator

@d1donlydfink d1donlydfink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot to address in here.

Same advice as always:

  • Slow down. Fast is not the answer in public API development.
  • Think through how your change effects existing installations
  • Be your own first reviewer. The best thing you could do is ask yourself "What is Dan going to say about this?" before handing it off to me.

@d1donlydfink d1donlydfink marked this pull request as draft June 27, 2025 18:44
Noravee and others added 4 commits June 30, 2025 14:45
- Resolve llm class in default_llm_factory when all factories fail
- Some test hocons
…_class_key' of https://github.com/leaf-ai/neuro-san into UN-3276_Support_for_user-defined_llm_in_hocon_file_with_class_key
@Noravee Noravee requested a review from d1donlydfink July 2, 2025 08:31

class UserSpecifiedLangChainLlmFactory(LangChainLlmFactory):
"""
A factory for constructing LLMs based on user-specified configurations provided under the "llm_config"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add UserSpecifiedLangChainLlmFactory that uses arguments from given by the user.

if self.llm_class:
# If config has "class", it is a user-specified llm so return config as is,
# and replace "StandardLangChainLlmFactory" with "UserSpecifiedLangChainLlmFactory".
self.llm_factories[0] = UserSpecifiedLangChainLlmFactory()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If llm_config has class, use UserSpecifiedLangChainLlmFactory.

try:
return_dict: Dict[str, Any] = await agent_executor.ainvoke(inputs, invoke_config)
except (APIError, BadRequestError, AuthenticationError, ChatGoogleGenerativeAIError) as api_error:
except (openai.APIError, anthropic.APIError, ChatGoogleGenerativeAIError) as api_error:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use APIError for anthropic as well.

retries = retries - 1
exception = key_error
backtrace = traceback.format_exc()
except TypeError as type_error:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add TypeError for non-existing arguments user may pass.

class_args_set: Set[str] = set(signature(method_class).parameters.keys())
pydantic_args: Set[str] = set()
# Check for if it is a class that extends pydantic BaseModel
if isclass(method_class) and issubclass(method_class, BaseModel):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if the class extends BaseModel. This is necessary since pydantic allows arguments to have alias.


"test_new_model_default_class.hocon": true,
"test_new_class.hocon": true,
"test_new_model_extended_class.hocon": true
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for testing only. It will not be in the final PR.

"llm_config": {
"class": "langchain_groq.chat_models.ChatGroq",
"model_name": "deepseek-r1-distill-llama-70b",
"temperature": 0.5
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for testing only. It will not be in the final PR.

"llm_config": {
"class": "openai",
"model_name": "gpt-4.1-mini",
"temperature": 0.5
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for testing only. It will not be in the final PR.

"llm_config": {
"class": "groq",
"model_name": "deepseek-r1-distill-llama-70b",
"temperature": 0.5,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for testing only. It will not be in the final PR.

@Noravee
Copy link
Collaborator Author

Noravee commented Jul 2, 2025

There are three types of LLM extensions using the class key:

  1. Using a new model with an existing class
    The class is already defined in the standard factory.
    Example: test_new_model_default_class.hocon
    → Uses StandardLangChainLlmFactory to instantiate the LLM.

  2. Using a new model with a new class
    The class is not pre-defined and is resolved dynamically.
    Example: test_new_class.hocon
    → Instantiated via DefaultLlmFactory.

  3. Using a new model with a class added to classes.factories
    The factory class explicitly extends LangChainLlmFactory.
    Example: test_new_model_extended_class.hocon
    → Instantiation is handled by the custom factory.

Note: Some checks fail due to missing dependencies (e.g., langchain-groq is not included in requirements.txt).
All temporary test files will be removed in the final PR.

Noravee added 3 commits July 3, 2025 11:50
- Refactor "check_invalid_arguments" into a util class
- Revert the error message detection in api key error checker
- Modified test_toolbox_factory
- Add test for ArgumentValidator
…_class_key' of https://github.com/leaf-ai/neuro-san into UN-3276_Support_for_user-defined_llm_in_hocon_file_with_class_key

# Try resolving via 'class' in config if factories failed
class_path: str = config.get("class")
if llm is None and found_exception is not None and class_path:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditions to instantiate using class in llm_config in agent network hocon are

  • Failed instantiation from the llm factories (thus llm is None)
  • Consequently, found_exception is not None
  • There is class in the config.


def create_base_chat_model_from_user_class(self, class_path: str, config: Dict[str, Any]) -> BaseLanguageModel:
"""
Create a BaseLanguageModel from the user-specified langchain model class.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor the logic of instantiating using "class" and user-defined args to create_base_chat_model_from_user_class() method.


class ArgumentValidator:
"""
A utility class for inspecting method and class arguments, particularly useful for
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor checking for invalid args to a utility class ArgumentValidator.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very nicely abstracted.

# ------------------------- Tests -------------------------------

class TestArgumentValidator:
"""Tests for the ArgumentValidator class and its static validation utilities."""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test for ArgumentValidator.

from neuro_san.internals.run_context.langchain.toolbox.toolbox_factory import ToolboxFactory

RESOLVER_PATH = "leaf_common.config.resolver.Resolver.resolve_class_in_module"
VALIDATIOR_PATH = (
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modify test for toolbox factory to reflect the refactoring.

@Noravee
Copy link
Collaborator Author

Noravee commented Jul 3, 2025

  • Refactor the instantiation of LLM from class in DefaultLlmFactory
  • Refactor the check for invalid args into a util class ArgumentValidator
  • Create test for ArgumentValidator

# To prevent this, we first fetch the default arguments for the given class from llm_info,
# then merge them with the user-provided config. This ensures all expected arguments are present,
# and the user’s config values take precedence over the defaults.
config_from_class_in_llm_info: Dict[str, Any] = self.get_chat_class_args(class_from_llm_config)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine user-specified config with the one in class in llm_info.

@staticmethod
def get_base_model_args(base_model_class: Type[BaseModel]) -> Set[str]:
"""
Extract all field names and aliases from a Pydantic BaseModel class.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good that you call out the BaseModel class as coming from Pydantic.
I always get confused thinking it's an LLM, but it's not, it's lower level than that.

Copy link
Collaborator

@d1donlydfink d1donlydfink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good... good enough at least to take out of Draft mode.
A couple more little things and we are there I think.

@Noravee Noravee marked this pull request as ready for review July 8, 2025 18:22
Copy link
Collaborator Author

@Noravee Noravee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes

  • Refactor ResolverUtil.create_instance() to ResolverUtil.create_class()
  • Use ResolverUtil in DefaultLlmFactory.


@staticmethod
def create_class(class_name: str, class_name_source: str, type_of_class: Type) -> Type:
"""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactor create_instance() to create_class() method because I need we need constructor with args for user-defined llm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

if not isinstance(instance, type_of_class):
raise ValueError(f"Class '{class_name}' from {class_name_source} "
"must be of type {type_of_class.__name__}")
if not issubclass(class_reference, type_of_class):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move check correct type to create_class().

raise ValueError("'class' in llm_config must be a string")

# Resolve the 'class'
llm_class: Type[BaseLanguageModel] = ResolverUtil.create_class(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ResolveUtil.create_class for the llm since we need to instantiate it with args.

# Check for invalid args and throw error if found
ArgumentValidator.check_invalid_args(llm_class, user_config)

return llm_class(**user_config)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM instantiation with args.

@Noravee Noravee requested a review from d1donlydfink July 8, 2025 18:34
raise ValueError(f"Class {llm_factory_class_name} in {llm_info_file} "
"must be of type LangChainLlmFactory")
# Resolve and instantiate the factory class
llm_factory = ResolverUtil.create_instance(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ResolverUtil.create_instance for resolve and instantiation of the factory class.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo. Nice.


with patch("leaf_common.config.resolver.Resolver.resolve_class_in_module") as mock_resolver, \
patch.object(factory, "_check_invalid_args") as mock_check_invalid:
with patch(RESOLVER_PATH) as mock_resolver, patch(VALIDATIOR_PATH) as mock_check_invalid:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo. fancy!

Copy link
Collaborator

@d1donlydfink d1donlydfink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great Nor. Overall things actually look a lot simpler as this has evolved over time. Nice work!

@Noravee Noravee merged commit c26d49b into main Jul 9, 2025
4 checks passed
@Noravee Noravee deleted the UN-3276_Support_for_user-defined_llm_in_hocon_file_with_class_key branch July 9, 2025 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants