| | |
| | import modal |
| | import os |
| | from pathlib import Path |
| |
|
| | |
| | app = modal.App("research-copilot") |
| |
|
| | |
| | image = modal.Image.debian_slim(python_version="3.11").pip_install([ |
| | "gradio>=4.0.0", |
| | "httpx", |
| | "aiohttp", |
| | "python-dotenv", |
| | "requests", |
| | "beautifulsoup4", |
| | "openai", |
| | "anthropic", |
| | ]) |
| |
|
| | |
| | code_mount = modal.Mount.from_local_dir( |
| | ".", |
| | remote_path="/app", |
| | condition=lambda path: path.suffix in [".py", ".txt", ".md"] |
| | ) |
| |
|
| | @app.function( |
| | image=image, |
| | mounts=[code_mount], |
| | allow_concurrent_inputs=100, |
| | timeout=3600, |
| | secrets=[ |
| | modal.Secret.from_name("research-copilot-secrets"), |
| | ] |
| | ) |
| | @modal.web_server(port=7860, startup_timeout=60) |
| | def run_gradio_app(): |
| | """Run the ResearchCopilot Gradio application""" |
| | import sys |
| | sys.path.append("/app") |
| | |
| | |
| | from ResearchCopilot.research_copilot import create_interface |
| | |
| | app = create_interface() |
| | app.launch( |
| | server_name="0.0.0.0", |
| | server_port=7860, |
| | share=False, |
| | show_error=True, |
| | enable_queue=True |
| | ) |
| |
|
| | |
| | @app.function( |
| | image=image, |
| | secrets=[modal.Secret.from_name("research-copilot-secrets")], |
| | timeout=300 |
| | ) |
| | async def search_perplexity(query: str, num_results: int = 5): |
| | """Search using Perplexity API""" |
| | import httpx |
| | import os |
| | |
| | api_key = os.getenv("PERPLEXITY_API_KEY") |
| | if not api_key: |
| | |
| | return { |
| | "results": [ |
| | { |
| | "title": f"Mock Result for: {query}", |
| | "url": "https://example.com/mock", |
| | "snippet": f"This is a mock result for the query: {query}", |
| | "source_type": "web" |
| | } |
| | ] |
| | } |
| | |
| | async with httpx.AsyncClient() as client: |
| | try: |
| | response = await client.post( |
| | "https://api.perplexity.ai/chat/completions", |
| | headers={ |
| | "Authorization": f"Bearer {api_key}", |
| | "Content-Type": "application/json" |
| | }, |
| | json={ |
| | "model": "llama-3.1-sonar-small-128k-online", |
| | "messages": [ |
| | {"role": "user", "content": f"Search for: {query}"} |
| | ], |
| | "max_tokens": 1000, |
| | "temperature": 0.2, |
| | "return_citations": True |
| | } |
| | ) |
| | |
| | if response.status_code == 200: |
| | data = response.json() |
| | return {"results": data.get("choices", [{}])[0].get("message", {}).get("content", "")} |
| | else: |
| | return {"error": f"API error: {response.status_code}"} |
| | |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| | @app.function( |
| | image=image, |
| | secrets=[modal.Secret.from_name("research-copilot-secrets")], |
| | timeout=300 |
| | ) |
| | async def search_google(query: str, num_results: int = 10): |
| | """Search using Google Custom Search API""" |
| | import httpx |
| | import os |
| | |
| | api_key = os.getenv("GOOGLE_API_KEY") |
| | search_engine_id = os.getenv("GOOGLE_SEARCH_ENGINE_ID") |
| | |
| | if not api_key or not search_engine_id: |
| | |
| | return { |
| | "results": [ |
| | { |
| | "title": f"Google Search: {query}", |
| | "url": "https://example.com/google-mock", |
| | "snippet": f"Mock Google search result for: {query}", |
| | "source_type": "web" |
| | } |
| | ] |
| | } |
| | |
| | async with httpx.AsyncClient() as client: |
| | try: |
| | response = await client.get( |
| | "https://www.googleapis.com/customsearch/v1", |
| | params={ |
| | "key": api_key, |
| | "cx": search_engine_id, |
| | "q": query, |
| | "num": min(num_results, 10) |
| | } |
| | ) |
| | |
| | if response.status_code == 200: |
| | data = response.json() |
| | results = [] |
| | for item in data.get("items", []): |
| | results.append({ |
| | "title": item.get("title", ""), |
| | "url": item.get("link", ""), |
| | "snippet": item.get("snippet", ""), |
| | "source_type": "web" |
| | }) |
| | return {"results": results} |
| | else: |
| | return {"error": f"Google API error: {response.status_code}"} |
| | |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| | @app.function( |
| | image=image, |
| | secrets=[modal.Secret.from_name("research-copilot-secrets")], |
| | timeout=600 |
| | ) |
| | async def summarize_with_claude(content: str, context: str = ""): |
| | """Summarize content using Claude API""" |
| | import httpx |
| | import os |
| | |
| | api_key = os.getenv("ANTHROPIC_API_KEY") |
| | if not api_key: |
| | |
| | return { |
| | "summary": f"Mock summary of content: {content[:100]}...", |
| | "key_points": ["Point 1", "Point 2", "Point 3"] |
| | } |
| | |
| | async with httpx.AsyncClient() as client: |
| | try: |
| | response = await client.post( |
| | "https://api.anthropic.com/v1/messages", |
| | headers={ |
| | "x-api-key": api_key, |
| | "Content-Type": "application/json", |
| | "anthropic-version": "2023-06-01" |
| | }, |
| | json={ |
| | "model": "claude-3-sonnet-20240229", |
| | "max_tokens": 1000, |
| | "messages": [ |
| | { |
| | "role": "user", |
| | "content": f"Summarize this content and extract key points:\n\nContext: {context}\n\nContent: {content}" |
| | } |
| | ] |
| | } |
| | ) |
| | |
| | if response.status_code == 200: |
| | data = response.json() |
| | content_text = data.get("content", [{}])[0].get("text", "") |
| | return { |
| | "summary": content_text, |
| | "key_points": ["AI-generated summary", "Professional analysis", "Comprehensive overview"] |
| | } |
| | else: |
| | return {"error": f"Claude API error: {response.status_code}"} |
| | |
| | except Exception as e: |
| | return {"error": str(e)} |
| |
|
| | if __name__ == "__main__": |
| | |
| | import subprocess |
| | subprocess.run(["python", "research_copilot.py"]) |