-
Notifications
You must be signed in to change notification settings - Fork 0
Add web search capability with Tavily (per-bot control) #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ecfc3a
587ec82
dade515
0f7e23e
fa99dcd
08318b0
9a10fd5
88ac8ad
969c3b7
523e3db
0e13458
8115cd3
174eecc
655ff37
48d0fdb
8fe8f5c
0965a6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| name: Lint and Test | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| frontend: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
| cache-dependency-path: front/package-lock.json | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| cd front | ||
| npm ci | ||
| npm install | ||
|
|
||
| - name: Run lint | ||
| run: | | ||
| cd front | ||
| npm run lint | ||
|
|
||
| - name: Run tests | ||
| run: | | ||
| cd front | ||
| npm test -- --passWithNoTests --coverage=false --watchAll=false | ||
|
|
||
| backend: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.12' | ||
| cache: 'pip' | ||
| cache-dependency-path: back/requirements.txt | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| cd back | ||
| pip install -r requirements.txt | ||
| pip install ruff | ||
|
|
||
| - name: Run ruff | ||
| run: | | ||
| cd back | ||
| ruff check bots/ server/ | ||
|
|
||
| - name: Run tests | ||
| run: | | ||
| cd back | ||
| pytest | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ __pycache__ | |
| db.sqlite3 | ||
| test.http | ||
| *.pem | ||
| venv/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,4 +6,4 @@ class BotsConfig(AppConfig): | |
| name = 'bots' | ||
|
|
||
| def ready(self): | ||
| import bots.signals | ||
| import bots.signals # noqa: F401 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Generated by Django 5.1.4 on 2026-04-02 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('bots', '0032_usagelimithit_modified_at'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='bot', | ||
| name='enable_web_search', | ||
| field=models.BooleanField(default=False), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,15 +2,33 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| from django.db import models | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from langchain_aws import ChatBedrock | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from langchain.schema import HumanMessage, SystemMessage, AIMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from langchain_core.messages import HumanMessage, SystemMessage, AIMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from langchain_core.tools import tool | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from langchain_core.callbacks.base import BaseCallbackHandler | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from langchain.agents import create_agent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from tavily import TavilyClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import base64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import boto3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .profile import Profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .bot import Bot | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .ai_model import AiModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| class TokenTracker(BaseCallbackHandler): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.input_tokens = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.output_tokens = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def on_llm_end(self, response, *, run_id=None, parent_run_id=None, **kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage_metadata = getattr(response, 'usage_metadata', None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if usage_metadata: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.input_tokens += usage_metadata.get('input_tokens', 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.output_tokens += usage_metadata.get('output_tokens', 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| S3_CLIENT = boto3.client('s3') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| S3_BUCKET = settings.AWS_STORAGE_BUCKET_NAME | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,7 +63,7 @@ def __init__(self, *args, **kwargs): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| self.ai = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __str__(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.title if self.user == None else self.user.email + ' - ' + self.title | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.title if self.user is None else self.user.email + ' - ' + self.title | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def use_default_model(self, ai=None): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,15 +81,129 @@ def get_response(self, ai=None): | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| message_list, contains_image = self.get_input() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check if any messages have image_filename and if the model supports images | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if contains_image and self.bot and 'image' not in self.bot.ai_model.supported_input_modalities: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.use_default_model(ai) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.user.user_account.over_limit(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "You have exceeded your daily limit. Please try again tomorrow or upgrade your subscription." | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = self.ai.invoke( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| message_list | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Use agent with tool calling if web search is enabled | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.bot and self.bot.enable_web_search and settings.TAVILY_API_KEY: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"Web search enabled for bot {self.bot.name}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| tavily_client = TavilyClient(api_key=settings.TAVILY_API_KEY) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+93
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @tool | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| def web_search(query: str) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Search the web for current information. Use this when you need up-to-date information or facts that may not be in your training data.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"🔍 WEB_SEARCH_TOOL_INVOKED: query='{query}'") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| results = tavily_client.search(query=query) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| num_results = len(results.get('results', [])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"🔍 WEB_SEARCH_SUCCESS: returned {num_results} results") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Format results as a readable string for the model | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if results.get('results'): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| formatted = "\n".join([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"- {r.get('title', 'No title')}: {r.get('content', '')[:200]}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for r in results['results'][:3] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug(f"🔍 WEB_SEARCH_FORMATTED_RESULTS:\n{formatted}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return formatted | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("🔍 WEB_SEARCH_NO_RESULTS: empty result set") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "No results found." | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"🔍 WEB_SEARCH_ERROR: {str(e)}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f"Error during search: {str(e)}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create chat model | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| chat_model = ChatBedrock(model_id=self.ai.model_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools = [web_search] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create modern agent with tool calling support | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # This is the recommended approach per LangChain docs | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent = create_agent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| model=chat_model, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools=tools, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| system_prompt=self.get_system_message(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| debug=settings.DEBUG | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Extract text input from message_list for agent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| agent_input = self._extract_agent_input(message_list) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"Invoking agent with input: {agent_input[:100]}...") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("🤖 AGENT_INVOKE_START: web_search tool available") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Invoke agent - the CompiledStateGraph handles tool loop internally | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = agent.invoke({"messages": [HumanMessage(content=agent_input)]}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("🤖 AGENT_INVOKE_COMPLETE: got response") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Extract response text from the agent result | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The response is a dict with 'messages' key containing final messages | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_text = "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage_metadata = {"input_tokens": 0, "output_tokens": 0} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(response, dict) and "messages" in response: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for msg in reversed(response["messages"]): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(msg, AIMessage): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_text = msg.content | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Extract token usage from the message metadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(msg, 'usage_metadata') and msg.usage_metadata: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage_metadata = msg.usage_metadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(response, dict) and "output" in response: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_text = response["output"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_text = str(response) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| message_order = self.messages.count() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| input_tokens = usage_metadata.get('input_tokens', 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tokens = usage_metadata.get('output_tokens', 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.messages.create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| text=response_text, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role='assistant', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| order=message_order, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| input_tokens=input_tokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| output_tokens=output_tokens | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+160
to
+172
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_text = response['output'] | |
| message_order = self.messages.count() | |
| self.messages.create( | |
| text=response_text, | |
| role='assistant', | |
| order=message_order, | |
| input_tokens=0, | |
| output_tokens=0 | |
| ) | |
| response_text = response['output'] | |
| usage_metadata = response.get('usage_metadata') if isinstance(response, dict) else None | |
| input_tokens = usage_metadata.get('input_tokens', 0) if usage_metadata else 0 | |
| output_tokens = usage_metadata.get('output_tokens', 0) if usage_metadata else 0 | |
| message_order = self.messages.count() | |
| self.messages.create( | |
| text=response_text, | |
| role='assistant', | |
| order=message_order, | |
| input_tokens=input_tokens, | |
| output_tokens=output_tokens | |
| ) | |
| self.input_tokens += input_tokens | |
| self.output_tokens += output_tokens |
Uh oh!
There was an error while loading. Please reload this page.