diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..45b8c4e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,143 @@ +# Hermes-Function-Calling Specialist Agents for Langchain Integration + +## Overview +This document outlines the specialist agents required for implementing a full Langchain integration with the Hermes-Function-Calling framework. The integration will extend the existing function calling capabilities to leverage Langchain's powerful abstractions for tools, chains, and agents. + +## Technology Stack Analysis + +### Core Technologies +- **Python 3.8+**: Primary development language +- **Langchain**: Framework for developing applications powered by LLMs +- **Transformers**: HuggingFace library for LLM integration +- **Pydantic**: Data validation and settings management + +### Key Dependencies +- `langchain`: Core Langchain library +- `langchain-core`: Core primitives for Langchain +- `transformers`: HuggingFace Transformers +- `pydantic`: Data validation +- `yfinance`: Yahoo Finance data (existing dependency) + +## Specialist Agents Required + +### 1. Core Framework Development Team + +#### **Langchain Integration Specialist** +- **Focus**: Integrating Langchain tools and agents with Hermes function calling +- **Skills**: Langchain framework, Python, function calling patterns +- **Responsibilities**: + - Implement Langchain tool adapters for existing Hermes functions + - Create Langchain-compatible agent for Hermes function calling + - Ensure compatibility between Langchain and Hermes execution loops + - Implement proper error handling and validation + +#### **Tool Wrapper Engineer** +- **Focus**: Converting existing functions to Langchain-compatible tools +- **Skills**: Langchain tools, Python decorators, function wrapping +- **Responsibilities**: + - Wrap existing functions in `functions.py` with Langchain `@tool` decorator + - Ensure proper schema generation for all wrapped tools + - Implement tool validation and error handling + - Create new tools using Langchain patterns + +### 2. Infrastructure & DevOps Team + +#### **Integration Testing Engineer** +- **Focus**: Testing the integration between Hermes and Langchain +- **Skills**: Testing frameworks, CI/CD, Langchain +- **Responsibilities**: + - Create integration tests for Langchain-Hermes interaction + - Validate tool calling and execution across both frameworks + - Implement regression tests for existing functionality + - Performance testing of integrated system + +### 3. Data & Evaluation Team + +#### **Evaluation Metrics Specialist** +- **Focus**: Measuring the effectiveness of Langchain integration +- **Skills**: Performance metrics, evaluation methodology +- **Responsibilities**: + - Define metrics for comparing Hermes-native vs Langchain-integrated function calling + - Implement evaluation suite for tool calling accuracy + - Benchmark performance differences + - Document best practices for Langchain integration + +### 4. Quality Assurance & Testing Team + +#### **Compatibility Testing Specialist** +- **Focus**: Ensuring backward compatibility and smooth integration +- **Skills**: Testing, compatibility analysis, regression testing +- **Responsibilities**: + - Verify that existing Hermes functionality remains intact + - Test Langchain tools with Hermes execution loop + - Validate schema generation and parsing for Langchain tools + - Identify and resolve compatibility issues + +## Development Workflow + +### Phase 1: Tool Integration +1. **Tool Wrapping**: Convert existing functions to Langchain-compatible tools +2. **Schema Generation**: Ensure proper OpenAPI schema generation for all tools +3. **Validation**: Implement validation for tool parameters and return values +4. **Testing**: Validate individual tools work correctly + +### Phase 2: Agent Integration +1. **Agent Creation**: Implement Langchain agent that can use Hermes functions +2. **Execution Loop**: Adapt Hermes execution loop to work with Langchain agents +3. **Error Handling**: Implement comprehensive error handling +4. **Testing**: Validate agent can successfully call and execute tools + +### Phase 3: Integration Testing +1. **Compatibility Testing**: Ensure existing functionality remains intact +2. **Performance Testing**: Measure performance impact of integration +3. **Regression Testing**: Verify no existing functionality is broken +4. **Documentation**: Create documentation for using Langchain integration + +## Key Integration Points + +### 1. Tool Registration +- Existing `@tool` decorated functions in `functions.py` should be compatible with Langchain +- Need to ensure `convert_to_openai_tool()` works correctly with all functions +- Implement any necessary adapters for functions that don't conform to Langchain patterns + +### 2. Schema Generation +- Use Langchain's built-in schema generation capabilities +- Ensure schemas are compatible with Hermes prompt formatting +- Validate that all required fields are properly included + +### 3. Execution Loop +- Adapt `functioncall.py` execution loop to work with Langchain agents +- Ensure tool results are properly formatted for both frameworks +- Implement proper error propagation between frameworks + +### 4. Prompt Management +- Integrate Langchain's prompt templating with Hermes' existing prompter +- Ensure system prompts properly describe Langchain tools +- Maintain compatibility with existing prompt formats + +## Performance Considerations + +- **Latency**: Minimize additional overhead from Langchain integration +- **Memory**: Efficient handling of Langchain objects and data structures +- **Concurrency**: Ensure thread-safety when using Langchain components +- **Scalability**: Design for horizontal scaling of integrated system + +## Security Requirements + +- **Input Validation**: Proper validation of all tool parameters +- **Output Sanitization**: Sanitize tool outputs before returning to model +- **Access Control**: Ensure proper access controls for sensitive functions +- **Error Handling**: Prevent information leakage through error messages + +## Verification & Validation Protocols + +### **Integration Verification Specialist** +- **Focus**: Ensuring proper integration between Hermes and Langchain +- **Skills**: Integration testing, validation protocols +- **Responsibilities**: + - Implement verification protocols for all integration points + - Define acceptance criteria for successful integration + - Conduct final validation before release + - Maintain integration quality standards + +This framework provides a structured approach to implementing Langchain integration with Hermes-Function-Calling, ensuring a robust, maintainable, and performant solution. \ No newline at end of file diff --git a/functions.py b/functions.py index 794627a..c679741 100644 --- a/functions.py +++ b/functions.py @@ -11,6 +11,7 @@ from langchain.tools import tool from langchain_core.utils.function_calling import convert_to_openai_tool + @tool def code_interpreter(code_markdown: str) -> dict | str: """ @@ -32,8 +33,8 @@ def code_interpreter(code_markdown: str) -> dict | str: """ try: # Extracting code from Markdown code block - code_lines = code_markdown.split('\n')[1:-1] - code_without_markdown = '\n'.join(code_lines) + code_lines = code_markdown.split("\n")[1:-1] + code_without_markdown = "\n".join(code_lines) # Create a new namespace for code execution exec_namespace = {} @@ -50,9 +51,11 @@ def code_interpreter(code_markdown: str) -> dict | str: except TypeError: # If the function requires arguments, attempt to call it with arguments from the namespace arg_names = inspect.getfullargspec(value).args - args = {arg_name: exec_namespace.get(arg_name) for arg_name in arg_names} + args = { + arg_name: exec_namespace.get(arg_name) for arg_name in arg_names + } result_dict[name] = value(**args) - elif not name.startswith('_'): # Exclude variables starting with '_' + elif not name.startswith("_"): # Exclude variables starting with '_' result_dict[name] = value return result_dict @@ -62,6 +65,7 @@ def code_interpreter(code_markdown: str) -> dict | str: inference_logger.error(error_message) return error_message + @tool def google_search_and_scrape(query: str) -> dict: """ @@ -74,50 +78,78 @@ def google_search_and_scrape(query: str) -> dict: list: A list of dictionaries containing the URL, text content, and table data for each scraped page. """ num_results = 2 - url = 'https://www.google.com/search' - params = {'q': query, 'num': num_results} - headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.3'} - - inference_logger.info(f"Performing google search with query: {query}\nplease wait...") + url = "https://www.google.com/search" + params = {"q": query, "num": num_results} + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.3" + } + + inference_logger.info( + f"Performing google search with query: {query}\nplease wait..." + ) response = requests.get(url, params=params, headers=headers) - soup = BeautifulSoup(response.text, 'html.parser') - urls = [result.find('a')['href'] for result in soup.find_all('div', class_='tF2Cxc')] - - inference_logger.info(f"Scraping text from urls, please wait...") + soup = BeautifulSoup(response.text, "html.parser") + urls = [ + result.find("a")["href"] for result in soup.find_all("div", class_="tF2Cxc") + ] + + inference_logger.info(f"Scraping text from urls, please wait...") [inference_logger.info(url) for url in urls] with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - futures = [executor.submit(lambda url: (url, requests.get(url, headers=headers).text if isinstance(url, str) else None), url) for url in urls[:num_results] if isinstance(url, str)] + futures = [ + executor.submit( + lambda url: ( + url, + requests.get(url, headers=headers).text + if isinstance(url, str) + else None, + ), + url, + ) + for url in urls[:num_results] + if isinstance(url, str) + ] results = [] for future in concurrent.futures.as_completed(futures): url, html = future.result() - soup = BeautifulSoup(html, 'html.parser') - paragraphs = [p.text.strip() for p in soup.find_all('p') if p.text.strip()] - text_content = ' '.join(paragraphs) - text_content = re.sub(r'\s+', ' ', text_content) - table_data = [[cell.get_text(strip=True) for cell in row.find_all('td')] for table in soup.find_all('table') for row in table.find_all('tr')] + soup = BeautifulSoup(html, "html.parser") + paragraphs = [p.text.strip() for p in soup.find_all("p") if p.text.strip()] + text_content = " ".join(paragraphs) + text_content = re.sub(r"\s+", " ", text_content) + table_data = [ + [cell.get_text(strip=True) for cell in row.find_all("td")] + for table in soup.find_all("table") + for row in table.find_all("tr") + ] if text_content or table_data: - results.append({'url': url, 'content': text_content, 'tables': table_data}) + results.append( + {"url": url, "content": text_content, "tables": table_data} + ) return results + @tool def get_current_stock_price(symbol: str) -> float: - """ - Get the current stock price for a given symbol. + """ + Get the current stock price for a given symbol. - Args: - symbol (str): The stock symbol. + Args: + symbol (str): The stock symbol. + + Returns: + float: The current stock price, or None if an error occurs. + """ + try: + stock = yf.Ticker(symbol) + # Use "regularMarketPrice" for regular market hours, or "currentPrice" for pre/post market + current_price = stock.info.get( + "regularMarketPrice", stock.info.get("currentPrice") + ) + return current_price if current_price else None + except Exception as e: + print(f"Error fetching current price for {symbol}: {e}") + return None - Returns: - float: The current stock price, or None if an error occurs. - """ - try: - stock = yf.Ticker(symbol) - # Use "regularMarketPrice" for regular market hours, or "currentPrice" for pre/post market - current_price = stock.info.get("regularMarketPrice", stock.info.get("currentPrice")) - return current_price if current_price else None - except Exception as e: - print(f"Error fetching current price for {symbol}: {e}") - return None @tool def get_stock_fundamentals(symbol: str) -> dict: @@ -147,24 +179,25 @@ def get_stock_fundamentals(symbol: str) -> dict: stock = yf.Ticker(symbol) info = stock.info fundamentals = { - 'symbol': symbol, - 'company_name': info.get('longName', ''), - 'sector': info.get('sector', ''), - 'industry': info.get('industry', ''), - 'market_cap': info.get('marketCap', None), - 'pe_ratio': info.get('forwardPE', None), - 'pb_ratio': info.get('priceToBook', None), - 'dividend_yield': info.get('dividendYield', None), - 'eps': info.get('trailingEps', None), - 'beta': info.get('beta', None), - '52_week_high': info.get('fiftyTwoWeekHigh', None), - '52_week_low': info.get('fiftyTwoWeekLow', None) + "symbol": symbol, + "company_name": info.get("longName", ""), + "sector": info.get("sector", ""), + "industry": info.get("industry", ""), + "market_cap": info.get("marketCap", None), + "pe_ratio": info.get("forwardPE", None), + "pb_ratio": info.get("priceToBook", None), + "dividend_yield": info.get("dividendYield", None), + "eps": info.get("trailingEps", None), + "beta": info.get("beta", None), + "52_week_high": info.get("fiftyTwoWeekHigh", None), + "52_week_low": info.get("fiftyTwoWeekLow", None), } return fundamentals except Exception as e: print(f"Error getting fundamentals for {symbol}: {e}") return {} + @tool def get_financial_statements(symbol: str) -> dict: """ @@ -184,6 +217,7 @@ def get_financial_statements(symbol: str) -> dict: print(f"Error fetching financial statements for {symbol}: {e}") return {} + @tool def get_key_financial_ratios(symbol: str) -> dict: """ @@ -203,6 +237,7 @@ def get_key_financial_ratios(symbol: str) -> dict: print(f"Error fetching key financial ratios for {symbol}: {e}") return {} + @tool def get_analyst_recommendations(symbol: str) -> pd.DataFrame: """ @@ -222,6 +257,7 @@ def get_analyst_recommendations(symbol: str) -> pd.DataFrame: print(f"Error fetching analyst recommendations for {symbol}: {e}") return pd.DataFrame() + @tool def get_dividend_data(symbol: str) -> pd.DataFrame: """ @@ -241,6 +277,7 @@ def get_dividend_data(symbol: str) -> pd.DataFrame: print(f"Error fetching dividend data for {symbol}: {e}") return pd.DataFrame() + @tool def get_company_news(symbol: str) -> pd.DataFrame: """ @@ -259,6 +296,7 @@ def get_company_news(symbol: str) -> pd.DataFrame: print(f"Error fetching company news for {symbol}: {e}") return pd.DataFrame() + @tool def get_technical_indicators(symbol: str) -> pd.DataFrame: """ @@ -277,6 +315,7 @@ def get_technical_indicators(symbol: str) -> pd.DataFrame: print(f"Error fetching technical indicators for {symbol}: {e}") return pd.DataFrame() + @tool def get_company_profile(symbol: str) -> dict: """ @@ -295,6 +334,7 @@ def get_company_profile(symbol: str) -> dict: print(f"Error fetching company profile for {symbol}: {e}") return {} + def get_openai_tools() -> List[dict]: functions = [ code_interpreter, @@ -307,8 +347,27 @@ def get_openai_tools() -> List[dict]: get_key_financial_ratios, get_analyst_recommendations, get_dividend_data, - get_technical_indicators + get_technical_indicators, ] tools = [convert_to_openai_tool(f) for f in functions] - return tools \ No newline at end of file + return tools + + +def get_langchain_tools(): + """Get all tools as LangChain tool objects""" + # All functions are already decorated with @tool, so we can return them directly + tools = [ + code_interpreter, + google_search_and_scrape, + get_current_stock_price, + get_company_news, + get_company_profile, + get_stock_fundamentals, + get_financial_statements, + get_key_financial_ratios, + get_analyst_recommendations, + get_dividend_data, + get_technical_indicators, + ] + return tools diff --git a/langchain_integration.py b/langchain_integration.py new file mode 100644 index 0000000..762a84c --- /dev/null +++ b/langchain_integration.py @@ -0,0 +1,210 @@ +""" +LangChain integration for Hermes Function Calling + +This module provides integration between Hermes Function Calling and LangChain, +allowing Hermes tools to be used within LangChain agents and vice versa. +""" + +import json +from typing import List, Dict, Any + +# Conditional imports to handle environments where LangChain is not available +try: + from langchain.tools import BaseTool + from langchain.agents import create_tool_calling_agent, AgentExecutor + from langchain.prompts import ChatPromptTemplate + from langchain_core.messages import HumanMessage, AIMessage, ToolMessage + + LANGCHAIN_AVAILABLE = True +except ImportError: + BaseTool = object + LANGCHAIN_AVAILABLE = False + +# Import Hermes functions +import functions +from functions import get_openai_tools + + +class HermesToolAdapter: + """Adapter to convert Hermes tools to LangChain tools""" + + def __init__(self): + # Check if LangChain is available + if not LANGCHAIN_AVAILABLE: + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + # Get all Hermes tools + self.tools = functions.get_langchain_tools() + + def get_tools(self) -> List[BaseTool]: + """Get all Hermes tools as LangChain tools""" + return self.tools + + +def create_hermes_agent(llm, tools: List[BaseTool]): + """ + Create a LangChain agent that can use Hermes tools + + Args: + llm: Language model to use for the agent + tools: List of LangChain tools (including Hermes tools) + + Returns: + AgentExecutor: Configured agent executor + """ + # Check if LangChain is available + if not LANGCHAIN_AVAILABLE: + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + # Create prompt for tool calling agent + prompt = ChatPromptTemplate.from_messages( + [ + ("system", "You are a helpful assistant with access to various tools."), + ("placeholder", "{chat_history}"), + ("human", "{input}"), + ("placeholder", "{agent_scratchpad}"), + ] + ) + + # Create the agent + agent = create_tool_calling_agent(llm, tools, prompt) + + # Create agent executor + agent_executor = AgentExecutor( + agent=agent, tools=tools, verbose=True, handle_parsing_errors=True + ) + + return agent_executor + + +def execute_tool_call(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: + """ + Execute a Hermes tool call + + Args: + tool_name: Name of the tool to execute + arguments: Arguments to pass to the tool + + Returns: + Dict containing the tool result + """ + # Get all Hermes tools + tools = functions.get_langchain_tools() + tool_dict = {tool.name: tool for tool in tools} + + # Find the tool + if tool_name not in tool_dict: + return { + "name": tool_name, + "error": f"Tool '{tool_name}' not found", + "content": None, + } + + # Execute the tool + try: + tool = tool_dict[tool_name] + result = tool.invoke(arguments) + return {"name": tool_name, "content": result, "error": None} + except Exception as e: + return {"name": tool_name, "error": str(e), "content": None} + + +def convert_hermes_result_to_langchain(tool_name: str, result: Dict[str, Any]) -> Any: + """ + Convert Hermes tool result to LangChain ToolMessage + + Args: + tool_name: Name of the tool that was executed + result: Result from the tool execution + + Returns: + ToolMessage: LangChain ToolMessage + """ + # Check if LangChain is available + if not LANGCHAIN_AVAILABLE: + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + if result.get("error"): + content = f"Error executing {tool_name}: {result['error']}" + else: + content = str(result.get("content", "")) + + return ToolMessage( + content=content, + tool_call_id=tool_name, # Simplified - in practice, you'd use a proper ID + ) + + +# Example usage function +def run_hermes_with_langchain(llm, query: str): + """ + Run a query using Hermes tools through LangChain agent + + Args: + llm: Language model to use + query: User query to process + + Returns: + Dict containing the final result + """ + # Check if LangChain is available + if not LANGCHAIN_AVAILABLE: + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + # Get Hermes tools + adapter = HermesToolAdapter() + tools = adapter.get_tools() + + # Create agent + agent_executor = create_hermes_agent(llm, tools) + + # Execute query + result = agent_executor.invoke({"input": query}) + + return result + + +# Fallback functions when LangChain is not available +def get_langchain_tools_fallback(): + """Fallback function to get tools when LangChain is not available""" + return [] + + +def create_hermes_agent_fallback(llm, tools): + """Fallback function to create agent when LangChain is not available""" + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + +def execute_tool_call_fallback( + tool_name: str, arguments: Dict[str, Any] +) -> Dict[str, Any]: + """Fallback function to execute tool call when LangChain is not available""" + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + +def convert_hermes_result_to_langchain_fallback( + tool_name: str, result: Dict[str, Any] +) -> Any: + """Fallback function to convert result when LangChain is not available""" + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) + + +def run_hermes_with_langchain_fallback(llm, query: str): + """Fallback function to run Hermes with LangChain when LangChain is not available""" + raise ImportError( + "LangChain is not available. Please install it to use this feature." + ) diff --git a/requirements.txt b/requirements.txt index 406a736..3f458b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,9 @@ torch==2.1.2 transformers>=4.38.1 bitsandbytes>=0.41.1 accelerate==0.27.2 -langchain==0.1.9 -pydantic==2.6.2 +langchain==1.2.3 +langchain-core==1.2.7 +pydantic==2.12.5 jsonschema==4.21.1 yfinance==0.2.36 pandas==2.2.0 diff --git a/test_langchain_integration.py b/test_langchain_integration.py new file mode 100644 index 0000000..c541ba8 --- /dev/null +++ b/test_langchain_integration.py @@ -0,0 +1,53 @@ +""" +Test script for LangChain integration with Hermes Function Calling + +This script demonstrates how to use Hermes tools with LangChain agents. +""" + +import sys +import os + +# Add the parent directory to the Python path to import Hermes modules +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Conditional imports to handle environments where LangChain is not available +try: + from langchain.llms import Ollama + from langchain_integration import HermesToolAdapter, run_hermes_with_langchain + + LANGCHAIN_AVAILABLE = True +except ImportError: + LANGCHAIN_AVAILABLE = False + print("LangChain is not available. Please install it to run this example.") + print("You can install it by running: pip install langchain") + + +def test_langchain_integration(): + """Test the LangChain integration with a simple query""" + if not LANGCHAIN_AVAILABLE: + print("Skipping test as LangChain is not available.") + return + + # Initialize LLM (using Ollama as an example) + # Make sure you have Ollama running and the model pulled + try: + llm = Ollama(model="phi3") + except Exception as e: + print(f"Failed to initialize LLM: {e}") + print("Make sure Ollama is running and the model is available.") + return + + # Test query + query = "What's the current stock price of Apple (AAPL)?" + + try: + # Run the query using Hermes tools through LangChain + result = run_hermes_with_langchain(llm, query) + print(f"Query: {query}") + print(f"Result: {result}") + except Exception as e: + print(f"Error running query: {e}") + + +if __name__ == "__main__": + test_langchain_integration()