junaid17 commited on
Commit
14f9874
·
verified ·
1 Parent(s): 8b99854

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +27 -0
  2. app.py +120 -0
  3. chatbot.py +175 -0
  4. requirements.txt +0 -0
  5. tools.py +262 -0
  6. utils.py +96 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Prevent Python from writing pyc files
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ WORKDIR /app
8
+
9
+ # Install system dependencies (required for sklearn / xgboost)
10
+ RUN apt-get update && apt-get install -y \
11
+ build-essential \
12
+ gcc \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy and install dependencies first (better caching)
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir --upgrade pip \
18
+ && pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copy application code
21
+ COPY . .
22
+
23
+ # Hugging Face expects port 7860
24
+ EXPOSE 7860
25
+
26
+ # Start FastAPI
27
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from tools import create_rag_tool, update_retriever
3
+ from chatbot import app as app_graph
4
+ from langchain_core.messages import HumanMessage
5
+ import os
6
+ from fastapi.responses import StreamingResponse, FileResponse
7
+ from langchain_core.messages import AIMessage
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel
10
+ from utils import TTS, STT, TTS2
11
+
12
+
13
+ app = FastAPI()
14
+
15
+ app.add_middleware(
16
+ CORSMiddleware,
17
+ allow_origins=["*"],
18
+ allow_credentials=True,
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ class TTSRequest(BaseModel):
24
+ text: str
25
+
26
+
27
+ UPLOAD_DIR = "uploads"
28
+
29
+ @app.get("/")
30
+ def health():
31
+ return {'Status' : 'The api is live and running'}
32
+
33
+ @app.post("/upload")
34
+ async def upload_file(file: UploadFile = File(...)):
35
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
36
+
37
+ file_path = os.path.join(UPLOAD_DIR, file.filename)
38
+
39
+ with open(file_path, "wb") as f:
40
+ f.write(await file.read())
41
+
42
+ update_retriever(file_path)
43
+
44
+ return {
45
+ "status": "success",
46
+ "filename": file.filename
47
+ }
48
+
49
+
50
+ @app.post("/chat")
51
+ async def chat(message: str, session_id: str = "default"):
52
+
53
+ async def event_generator():
54
+ async for chunk in app_graph.astream(
55
+ {"messages": [HumanMessage(content=message)]},
56
+ config={"configurable": {"thread_id": session_id}},
57
+ stream_mode="messages" ):
58
+ if len(chunk) >= 1:
59
+ message_chunk = chunk[0] if isinstance(chunk, tuple) else chunk
60
+ if hasattr(message_chunk, 'content') and message_chunk.content:
61
+ data = str(message_chunk.content).replace("\n", "\\n")
62
+ yield f"data: {data}\n\n"
63
+
64
+ return StreamingResponse(
65
+ event_generator(),
66
+ media_type="text/event-stream",
67
+ headers={
68
+ "Cache-Control": "no-cache",
69
+ "X-Accel-Buffering": "no",
70
+ },
71
+ )
72
+
73
+ # ---------------- STT ---------------- #
74
+
75
+ @app.post("/stt")
76
+ async def transcribe_audio(file: UploadFile = File(...)):
77
+ try:
78
+ return await STT(file)
79
+ except Exception as e:
80
+ raise HTTPException(status_code=500, detail=str(e))
81
+
82
+
83
+ @app.post("/tts")
84
+ async def generate_tts(request: TTSRequest):
85
+ try:
86
+ if not request.text.strip():
87
+ raise HTTPException(status_code=400, detail="Text is empty")
88
+
89
+ audio_path = await TTS(text=request.text)
90
+
91
+ if not os.path.exists(audio_path):
92
+ raise HTTPException(status_code=500, detail="Audio file not created")
93
+
94
+ return FileResponse(
95
+ path=audio_path,
96
+ media_type="audio/mpeg",
97
+ filename="speech.mp3"
98
+ )
99
+
100
+ except Exception as e:
101
+ raise HTTPException(status_code=500, detail=str(e))
102
+
103
+ # -------------------------- TTS2 -------------------------
104
+
105
+ @app.post("/tts2")
106
+ async def generate_tts2(text: str):
107
+ try:
108
+ audio_path = await TTS(text=text)
109
+
110
+ if not os.path.exists(audio_path):
111
+ raise HTTPException(status_code=500, detail="Audio generation failed")
112
+
113
+ return FileResponse(
114
+ path=audio_path,
115
+ media_type="audio/wav",
116
+ filename="speech.wav"
117
+ )
118
+
119
+ except Exception as e:
120
+ raise HTTPException(status_code=500, detail=str(e))
chatbot.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from typing import TypedDict, Annotated, List
3
+
4
+ # =========================
5
+ # LangChain / LangGraph
6
+ # =========================
7
+ from langchain_core.messages import (
8
+ BaseMessage,
9
+ HumanMessage,
10
+ SystemMessage
11
+ )
12
+ from langgraph.checkpoint.memory import MemorySaver
13
+ from tools import retriever, create_rag_tool, arxiv_search, calculator, get_stock_price, wikipedia_search, tavily_search, convert_currency, unit_converter, get_news, get_joke, get_quote, get_weather
14
+ from langchain_openai import ChatOpenAI
15
+ from langgraph.graph import StateGraph, START, END
16
+ from langgraph.graph.message import add_messages
17
+ from langgraph.prebuilt import ToolNode, tools_condition
18
+ from dotenv import load_dotenv
19
+ import os
20
+ load_dotenv()
21
+
22
+
23
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
24
+
25
+ # =====================================================
26
+ # 1️⃣ SYSTEM PROMPT
27
+ # =====================================================
28
+
29
+ SYSTEM_PROMPT = SystemMessage(
30
+ content="""
31
+ You are an intelligent AI assistant built inside a LangGraph-based system created by Junaid (also known as Juddy).
32
+
33
+ Your purpose is to provide accurate, helpful, and reliable responses using reasoning, tools, memory, and document-based retrieval when appropriate.
34
+
35
+ ━━━━━━━━━━━━━━━━━━━━━━
36
+ 🔹 ABOUT YOUR CREATOR
37
+ ━━━━━━━━━━━━━━━━━━━━━━
38
+ - You were designed and iteratively improved by Junaid as part of an evolving AI engineering project.
39
+ - Your development journey includes:
40
+ 1. A basic conversational chatbot
41
+ 2. Memory integration
42
+ 3. Streaming responses
43
+ 4. Tool usage (RAG, STT, TTS)
44
+ - You may acknowledge this when asked, but always focus on helping the user.
45
+
46
+ ━━━━━━━━━━━━━━━━━━━━━━
47
+ 🔹 CORE BEHAVIOR
48
+ ━━━━━━━━━━━━━━━━━━━━━━
49
+ - Be helpful, accurate, concise, and professional.
50
+ - Prefer clarity over verbosity.
51
+ - Maintain conversational context using memory.
52
+ - Avoid hallucinations at all costs.
53
+ - If information is uncertain or missing, say so clearly.
54
+
55
+ ━━━━━━━━━━━━━━━━━━━━━━
56
+ 🔹 TOOL USAGE PRIORITY (VERY IMPORTANT)
57
+ ━━━━━━━━━━━━━━━━━━━━━━
58
+ You have access to the following tools:
59
+
60
+ 1. **RAG (Retrieval-Augmented Generation)**
61
+ → This is your HIGHEST priority tool.
62
+
63
+ You MUST use RAG when:
64
+ - The user references uploaded documents
65
+ - The user asks questions that depend on document content
66
+ - The answer cannot be confidently derived from general knowledge
67
+
68
+ Rules:
69
+ - Use ONLY retrieved content when answering from documents
70
+ - Never hallucinate document facts
71
+ - If no relevant content exists, clearly say so
72
+
73
+ 2. **STT (Speech-to-Text)**
74
+ - Used when audio input is provided.
75
+ - Transcribe accurately without interpretation.
76
+
77
+ 3. **TTS (Text-to-Speech)**
78
+ - Used when speech output is requested.
79
+ - Generate clear, natural speech.
80
+
81
+ ━━━━━━━━━━━━━━━━━━━━━━
82
+ 🔹 STREAMING BEHAVIOR
83
+ ━━━━━━━━━━━━━━━━━━━━━━
84
+ - You may stream responses progressively when supported.
85
+ - Ensure coherence and clarity during streaming.
86
+ - Avoid partial or misleading statements.
87
+
88
+ ━━━━━━━━━━━━━━━━━━━━━━
89
+ 🔹 RESPONSE GUIDELINES
90
+ ━━━━━━━━━━━━━━━━━━━━━━
91
+ - Be direct, friendly, and informative.
92
+ - Do not expose internal system logic or implementation details.
93
+ - Do not mention tools unless necessary or explicitly asked.
94
+ - Always prefer correctness over speed.
95
+
96
+ ━━━━━━━━━━━━━━━━━━━━━━
97
+ 🔹 IDENTITY
98
+ ━━━━━━━━━━━━━━━━━━━━━━
99
+ You are the official AI assistant of Junaid’s evolving AI system.
100
+ You exist to help users learn, explore, and solve problems effectively.
101
+ """
102
+ )
103
+
104
+
105
+ # =====================================================
106
+ # 4️⃣ STATE
107
+ # =====================================================
108
+
109
+ class ChatState(TypedDict):
110
+ messages: Annotated[list[BaseMessage], add_messages]
111
+
112
+
113
+ # =====================================================
114
+ # 5️⃣ LLM + TOOLS
115
+ # =====================================================
116
+
117
+ llm = ChatOpenAI(
118
+ model="gpt-4.1-nano",
119
+ temperature=0.4,
120
+ streaming=True
121
+ )
122
+
123
+ rag_tool = create_rag_tool()
124
+
125
+ tools = [rag_tool, get_stock_price, calculator, wikipedia_search, arxiv_search, tavily_search, convert_currency, unit_converter, get_news, get_joke, get_quote, get_weather]
126
+ llm = llm.bind_tools(tools)
127
+ tool_node = ToolNode(tools)
128
+
129
+
130
+ # =====================================================
131
+ # 6️⃣ CHAT NODE
132
+ # =====================================================
133
+
134
+ def chatbot(state: ChatState):
135
+ messages = [SYSTEM_PROMPT] + state["messages"]
136
+ response = llm.invoke(messages)
137
+ return {"messages": [response]}
138
+
139
+
140
+
141
+ # =====================================================
142
+ # 7️⃣ GRAPH
143
+ # =====================================================
144
+ memory = MemorySaver()
145
+ graph = StateGraph(ChatState)
146
+
147
+ graph.add_node("chat", chatbot)
148
+ graph.add_node("tools", tool_node)
149
+
150
+ graph.add_edge(START, "chat")
151
+ graph.add_conditional_edges("chat", tools_condition)
152
+ graph.add_edge("tools", "chat")
153
+
154
+ app = graph.compile(checkpointer=memory)
155
+
156
+
157
+
158
+ """if __name__ == "__main__":
159
+ print("\n🤖 LangGraph RAG Chatbot (with MemorySaver)\n")
160
+
161
+ THREAD_ID = "user_1" # 🔥 This preserves memory
162
+
163
+ while True:
164
+ user_input = input("You: ")
165
+
166
+ if user_input.lower() in {"exit", "quit"}:
167
+ break
168
+
169
+ result = app.invoke(
170
+ {"messages": [HumanMessage(content=user_input)]},
171
+ config={"configurable": {"thread_id": THREAD_ID}}
172
+ )
173
+
174
+ print("Bot:", result["messages"][-1].content)
175
+ """
requirements.txt ADDED
Binary file (532 Bytes). View file
 
tools.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
3
+ from langchain_community.vectorstores import FAISS
4
+ from langchain_community.document_loaders import PyPDFLoader
5
+ from langchain_openai import OpenAIEmbeddings
6
+ from langchain_community.tools import WikipediaQueryRun, ArxivQueryRun
7
+ from langchain_community.utilities import WikipediaAPIWrapper, ArxivAPIWrapper
8
+ from langchain_core.tools import tool
9
+ from langchain_community.tools.tavily_search import TavilySearchResults
10
+ from dotenv import load_dotenv
11
+ import os
12
+ import requests
13
+
14
+ load_dotenv()
15
+
16
+ API_KEY = os.getenv("ALPHAVANTAGE_API_KEY")
17
+ NEWS_API_KEY = os.getenv("NEWS_API_KEY")
18
+ WEATHER_API_KEY = os.getenv("WEATHER_API_KEY")
19
+ NEWS_API_KEY = os.getenv("NEWS_API_KEY")
20
+
21
+ # -------------------------------
22
+ # GLOBAL RETRIEVER
23
+ # -------------------------------
24
+ retriever = None
25
+
26
+
27
+ def build_vectorstore(path: str):
28
+ loader = PyPDFLoader(path)
29
+ docs = loader.load()
30
+
31
+ splitter = RecursiveCharacterTextSplitter(
32
+ chunk_size=500,
33
+ chunk_overlap=100
34
+ )
35
+
36
+ split_docs = splitter.split_documents(docs)
37
+
38
+ embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
39
+ return FAISS.from_documents(split_docs, embeddings)
40
+
41
+
42
+ def update_retriever(pdf_path: str):
43
+ global retriever
44
+ vectorstore = build_vectorstore(pdf_path)
45
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
46
+
47
+
48
+ # -------------------------------
49
+ # RAG TOOL
50
+ # -------------------------------
51
+ def create_rag_tool():
52
+
53
+ @tool
54
+ def rag_search(query: str) -> str:
55
+ """
56
+ Retrieve relevant information from uploaded documents.
57
+ """
58
+ if retriever is None:
59
+ return "No document uploaded yet."
60
+
61
+ docs = retriever.invoke(query)
62
+
63
+ if not docs:
64
+ return "No relevant information found."
65
+
66
+ return "\n\n".join(d.page_content for d in docs)
67
+
68
+ return rag_search
69
+
70
+ @tool
71
+ def arxiv_search(query: str) -> dict:
72
+ """
73
+ Search arXiv for academic papers related to the query.
74
+ """
75
+ try:
76
+ arxiv = ArxivQueryRun(api_wrapper=ArxivAPIWrapper())
77
+ results = arxiv.run(query)
78
+ return {"query": query, "results": results}
79
+ except Exception as e:
80
+ return {"error": str(e)}
81
+
82
+ @tool
83
+ def calculator(first_num: float, second_num: float, operation: str) -> dict:
84
+ """
85
+ Perform a basic arithmetic operation on two numbers.
86
+ Supported operations: add, sub, mul, div
87
+ """
88
+ try:
89
+ if operation == "add":
90
+ result = first_num + second_num
91
+ elif operation == "sub":
92
+ result = first_num - second_num
93
+ elif operation == "mul":
94
+ result = first_num * second_num
95
+ elif operation == "div":
96
+ if second_num == 0:
97
+ return {"error": "Division by zero is not allowed"}
98
+ result = first_num / second_num
99
+ else:
100
+ return {"error": f"Unsupported operation '{operation}'"}
101
+
102
+ return {"first_num": first_num, "second_num": second_num, "operation": operation, "result": result}
103
+ except Exception as e:
104
+ return {"error": str(e)}
105
+ @tool
106
+ def tavily_search(query: str) -> dict:
107
+ """
108
+ Perform a web search using Tavily,
109
+ also use it to get weather information,
110
+ Returns up to 5 search results.
111
+ """
112
+ try:
113
+ search = TavilySearchResults(max_results=5)
114
+ results = search.run(query)
115
+ return {"query": query, "results": results}
116
+ except Exception as e:
117
+ return {"error": str(e)}
118
+
119
+
120
+ @tool
121
+ def get_stock_price(symbol: str) -> dict:
122
+ """
123
+ Fetch latest stock price for a given symbol (e.g. 'AAPL', 'TSLA')
124
+ using Alpha Vantage with API key in the URL.
125
+ """
126
+ url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={API_KEY}"
127
+ r = requests.get(url)
128
+ return r.json()
129
+
130
+ @tool
131
+ def wikipedia_search(query: str) -> dict:
132
+ """
133
+ Search Wikipedia for a given query and return results.
134
+ """
135
+ try:
136
+ wiki = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
137
+ results = wiki.run(query)
138
+ return {"query": query, "results": results}
139
+ except Exception as e:
140
+ return {"error": str(e)}
141
+
142
+ @tool
143
+ def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
144
+ """
145
+ Convert amount from one currency to another using Frankfurter API.
146
+ Example: convert_currency(100, "USD", "EUR")
147
+ """
148
+ try:
149
+ url = f"https://api.frankfurter.app/latest?amount={amount}&from={from_currency}&to={to_currency}"
150
+ r = requests.get(url)
151
+ return r.json()
152
+ except Exception as e:
153
+ return {"error": str(e)}
154
+ @tool
155
+
156
+
157
+ def unit_converter(value: float, from_unit: str, to_unit: str) -> dict:
158
+ """
159
+ Convert between metric/imperial units (supports: km<->miles, kg<->lbs, C<->F).
160
+ Example: unit_converter(10, "km", "miles")
161
+ """
162
+ try:
163
+ conversions = {
164
+ ("km", "miles"): lambda x: x * 0.621371,
165
+ ("miles", "km"): lambda x: x / 0.621371,
166
+ ("kg", "lbs"): lambda x: x * 2.20462,
167
+ ("lbs", "kg"): lambda x: x / 2.20462,
168
+ ("C", "F"): lambda x: (x * 9/5) + 32,
169
+ ("F", "C"): lambda x: (x - 32) * 5/9
170
+ }
171
+ if (from_unit, to_unit) not in conversions:
172
+ return {"error": f"Unsupported conversion: {from_unit} -> {to_unit}"}
173
+ result = conversions[(from_unit, to_unit)](value)
174
+ return {"value": value, "from": from_unit, "to": to_unit, "result": result}
175
+ except Exception as e:
176
+ return {"error": str(e)}
177
+
178
+
179
+
180
+ @tool
181
+ def get_news(query: str) -> dict:
182
+ """
183
+ Fetch latest news headlines for a given query.
184
+ Example: get_news("artificial intelligence")
185
+ """
186
+ try:
187
+ url = f"https://newsapi.org/v2/everything?q={query}&apiKey={NEWS_API_KEY}&language=en"
188
+ r = requests.get(url)
189
+ return r.json()
190
+ except Exception as e:
191
+ return {"error": str(e)}
192
+
193
+
194
+ @tool
195
+ def get_joke(category: str = "Any") -> dict:
196
+ """
197
+ Get a random joke. Categories: Programming, Misc, Pun, Spooky, Christmas, Any
198
+ Example: get_joke("Programming")
199
+ """
200
+ try:
201
+ url = f"https://v2.jokeapi.dev/joke/{category}"
202
+ r = requests.get(url)
203
+ return r.json()
204
+ except Exception as e:
205
+ return {"error": str(e)}
206
+
207
+ @tool
208
+ def get_quote(tag: str = "") -> dict:
209
+ """
210
+ Fetch a random quote. Optionally filter by tag (e.g., 'inspirational', 'technology').
211
+ Example: get_quote("inspirational")
212
+ """
213
+ try:
214
+ url = f"https://api.quotable.io/random"
215
+ if tag:
216
+ url += f"?tags={tag}"
217
+ r = requests.get(url)
218
+ return r.json()
219
+ except Exception as e:
220
+ return {"error": str(e)}
221
+
222
+ @tool
223
+ def get_weather(city: str) -> dict:
224
+ """
225
+ Get current weather for a given city using WeatherAPI.com.
226
+ Example: get_weather("London")
227
+ """
228
+ try:
229
+ url = f"http://api.weatherapi.com/v1/current.json?key={WEATHER_API_KEY}&q={city}&aqi=no"
230
+ r = requests.get(url)
231
+ data = r.json()
232
+
233
+ if "error" in data:
234
+ return {"error": data["error"]["message"]}
235
+
236
+ return {
237
+ "location": data["location"]["name"],
238
+ "country": data["location"]["country"],
239
+ "temperature_c": data["current"]["temp_c"],
240
+ "temperature_f": data["current"]["temp_f"],
241
+ "condition": data["current"]["condition"]["text"],
242
+ "humidity": data["current"]["humidity"],
243
+ "wind_kph": data["current"]["wind_kph"],
244
+ "wind_dir": data["current"]["wind_dir"]
245
+ }
246
+ except Exception as e:
247
+ return {"error": str(e)}
248
+
249
+
250
+
251
+ @tool
252
+ def get_news(query: str) -> dict:
253
+ """
254
+ Fetch latest news headlines for a given query.
255
+ Example: get_news("artificial intelligence")
256
+ """
257
+ try:
258
+ url = f"https://newsapi.org/v2/everything?q={query}&apiKey={NEWS_API_KEY}&language=en"
259
+ r = requests.get(url)
260
+ return r.json()
261
+ except Exception as e:
262
+ return {"error": str(e)}
utils.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from uuid import uuid4
3
+ import edge_tts
4
+ from groq import Groq
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ client = Groq()
10
+
11
+ # ==================================================
12
+ # 🎙️ TEXT TO SPEECH (FIXED VOICE)
13
+ # ==================================================
14
+
15
+ DEFAULT_VOICE = "en-US-MichelleNeural"
16
+
17
+ async def TTS(
18
+ text: str,
19
+ output_dir: str = "tts_outputs",
20
+ rate: str = "+0%",
21
+ pitch: str = "+0Hz"
22
+ ) -> str:
23
+
24
+ if not text.strip():
25
+ raise ValueError("Empty text")
26
+
27
+ os.makedirs(output_dir, exist_ok=True)
28
+
29
+ filename = f"{uuid4().hex}.mp3"
30
+ output_path = os.path.join(output_dir, filename)
31
+
32
+ communicate = edge_tts.Communicate(
33
+ text=text,
34
+ voice=DEFAULT_VOICE,
35
+ rate=rate,
36
+ pitch=pitch
37
+ )
38
+
39
+ await communicate.save(output_path)
40
+ return output_path
41
+
42
+
43
+ # ==================================================
44
+ # 🎧 SPEECH TO TEXT
45
+ # ==================================================
46
+
47
+ async def STT(audio_file):
48
+ os.makedirs("uploads", exist_ok=True)
49
+ file_path = f"uploads/{uuid4().hex}.wav"
50
+
51
+ with open(file_path, "wb") as f:
52
+ f.write(await audio_file.read())
53
+
54
+ with open(file_path, "rb") as f:
55
+ transcription = client.audio.transcriptions.create(
56
+ file=f,
57
+ model="whisper-large-v3-turbo",
58
+ response_format="verbose_json",
59
+ temperature=0.0
60
+ )
61
+
62
+ return {
63
+ "text": transcription.text,
64
+ "segments": transcription.segments,
65
+ "language": transcription.language
66
+ }
67
+
68
+
69
+ # ==================================================
70
+ # 🎧 SPEECH TO TEXT 2
71
+ # ==================================================
72
+
73
+ async def TTS2(
74
+ text: str,
75
+ output_dir: str = "tts_outputs",
76
+ voice: str = "austin",
77
+ model: str = "canopylabs/orpheus-v1-english"
78
+ ) -> str:
79
+ if not text.strip():
80
+ raise ValueError("Text cannot be empty")
81
+
82
+ os.makedirs(output_dir, exist_ok=True)
83
+
84
+ filename = f"{uuid4().hex}.wav"
85
+ output_path = os.path.join(output_dir, filename)
86
+
87
+ response = client.audio.speech.create(
88
+ model=model,
89
+ voice=voice,
90
+ input=text,
91
+ response_format="wav"
92
+ )
93
+
94
+ response.stream_to_file(output_path)
95
+
96
+ return output_path