File size: 6,845 Bytes
c663214
e288957
 
 
ac0f508
e288957
a16f585
ec2661f
abc1112
 
5845cb8
ac0f508
3416046
af1f85e
a16f585
6d34792
 
afd11f2
e288957
ec2661f
e288957
c663214
 
7ff993c
e288957
6e06bba
39d720c
ac0f508
c663214
 
abc1112
e288957
c663214
3416046
 
 
c663214
e288957
 
c663214
1eadb17
c663214
 
 
 
1eadb17
c663214
 
 
 
 
5659eb0
abc1112
 
 
 
 
 
 
 
 
3416046
abc1112
e288957
abc1112
 
 
afd11f2
c663214
e288957
c663214
 
e288957
 
 
c663214
e288957
 
c999b95
 
4fd7379
 
 
 
 
afd11f2
4fd7379
 
 
 
 
6d34792
 
c999b95
 
 
b392389
e288957
6d34792
 
 
 
 
 
37f768f
 
 
6d34792
37f768f
 
6d34792
37f768f
 
 
 
 
 
 
 
 
6d34792
37f768f
 
 
6d34792
 
 
 
37f768f
 
 
 
6d34792
37f768f
 
 
 
 
 
6d34792
37f768f
6d34792
37f768f
 
 
 
 
 
6d34792
37f768f
 
 
c663214
ac0f508
 
37f768f
 
 
 
a16f585
 
6d34792
90979e0
 
a16f585
ac0f508
6513634
 
 
 
 
 
ac0f508
a16f585
ac0f508
6513634
ac0f508
37f768f
ac0f508
c663214
ac0f508
4fd7379
c663214
 
 
4fd7379
ac0f508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457c7a2
 
 
 
 
ac0f508
 
 
 
6db9b41
ac0f508
c663214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import smtplib
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph,START,END
from langgraph.prebuilt import tools_condition
from langgraph.graph.message import add_messages 
from langchain_core.tools import tool
from dotenv import load_dotenv
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
from langchain_tavily import TavilySearch
from langchain_groq import ChatGroq
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command, interrupt
from google_auth_helpers import AUTH_REQUIRED_PREFIX
from drive_tools import search_and_download_doc_tool
from calendar_tools import create_calendar_event_tool
import os


# ==================== LOAD ENV =======================
# memory = MemorySaver()

load_dotenv()
base_model = "openai/gpt-oss-120b"
api = os.getenv("GROQ_API_KEY")
tavily = os.getenv("TAVILY_API")
SENDER_EMAIL = os.getenv("SENDER_EMAIL")
SENDER_PASSWORD = os.getenv("SENDER_PASSWORD")
SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")

# ==================== LLM =======================
llm = ChatGroq(
    api_key = api,
    model = base_model,
    temperature=0.3
)

# ==================== TOOL =======================

@tool
def send_email_tool(to_email: str, subject: str, body: str) -> str:
    """
    Sends an email to a recipient.

    Args:
        to_email: Recipient email address
        subject: Email subject
        body: Email body content
    """

    try:
        message = Mail(
            from_email=SENDER_EMAIL,
            to_emails=to_email,
            subject=subject,
            plain_text_content=body,
        )
        sg = SendGridAPIClient(SENDGRID_API_KEY)
        sg.send(message)

        return f"Email successfully sent to {to_email}"

    except Exception as e:
        return f"Failed to send email: {str(e)}"
    
tools = [search_and_download_doc_tool, send_email_tool, create_calendar_event_tool]
llm_with_tools = llm.bind_tools(tools)

# ==================== STATE =======================
class State(TypedDict):
    messages: Annotated[list,add_messages]


# ==================== NODES =======================

def chatbot(state:State):
    response = llm_with_tools.invoke([
    SystemMessage(content="""
    You are an AI assistant with access to tools.

    Available tools:
    1. send_email_tool β†’ Use when the user wants to send an email.
    2. search_and_download_doc_tool β†’ Use when the user wants to find or download a document from Google Drive.
    3. create_calendar_event_tool β†’ Use when the user wants to schedule a meeting, send a calendar invite, or create a Google Meet.

    Rules:
    - Always call the appropriate tool when the request requires action.
    - Do NOT respond with plain text if an action is required.
    - After tool execution, summarize the result for the user.
    - Never invent or fabricate Google OAuth URLs. If authentication is required, the tool
      result or system interrupt will provide the real sign-in link.
    """),
        *state["messages"]
    ])
    return {"messages":[response]}

_AUTH_PRODUCT_LABELS = {
    "search_and_download_doc_tool": "Google Drive",
    "create_calendar_event_tool": "Google Calendar",
}


def handle_tools(state: State):
    """
    Custom tool-execution node that intercepts AUTH_REQUIRED:: sentinels
    from Google tools and surfaces them via LangGraph interrupt.
    """
    last_message: AIMessage = state["messages"][-1]

    results = []
    for tool_call in last_message.tool_calls:
        matched_tool = next(
            (t for t in tools if t.name == tool_call["name"]), None
        )
        if matched_tool is None:
            result_content = f"Unknown tool: {tool_call['name']}"
        else:
            result_content = matched_tool.invoke(tool_call["args"])

        if isinstance(result_content, str) and result_content.startswith(AUTH_REQUIRED_PREFIX):
            first_line = result_content.split("\n")[0]
            auth_url = first_line.removeprefix(AUTH_REQUIRED_PREFIX).strip()
            product = _AUTH_PRODUCT_LABELS.get(
                tool_call["name"], "Google (Drive & Calendar)"
            )

            interrupt({
                "type": "auth_required",
                "auth_url": auth_url,
                "message": (
                    f"πŸ” {product} access is required. "
                    "Please authenticate by visiting the link below, then retry your request.\n\n"
                    f"πŸ‘‰ {auth_url}"
                ),
            })
            result_content = (
                "Authentication flow initiated. Once you have completed Google sign-in, "
                "please repeat your request."
            )

        results.append(
            ToolMessage(
                content=result_content,
                tool_call_id=tool_call["id"],
            )
        )

    return {"messages": results}
 

# ==================== GRAPH =======================

# Adding Node
memory = MemorySaver()

graph_builder=StateGraph(State)

graph_builder.add_node("chatbot", chatbot)

graph_builder.add_node("tools", handle_tools)

graph_builder.add_edge(START, "chatbot")

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {
        "tools": "tools",
        "__end__": END
    }
)


graph_builder.add_edge("tools","chatbot")

graph=graph_builder.compile(checkpointer=memory)

# ==================== ENTRY FUNCTION =======================

def run_agent(user_input: str):
    result = graph.invoke({
        "messages": [HumanMessage(content=user_input)]
    })
    return result["messages"][-1].content

# Start
# LLM + promt -> Chatbot 
# 
# 
# 
# External Yools  Tool Node
# Make a tool Call || Tavily || -><- 
# 
# 
# End 
# How does chatbot know the when to use tools ?
# LLM binds with the tools : 
# Addition function added as tool to the LLM 
# Doc String is used to know what are thre inputs and arguments : If they match LLM make a call to Tool
# Instead of relying on its own response.
# 
# ReACT agent aplits the query into multiple statements and repeatedly solves query part by part
# 1. Act
# 2. Observe
# 3. Reason
# #
# Binding tools with LLMs #




# Stategraph



# tokenizer = AutoTokenizer.from_pretrained(
#     base_model,
#     use_auth_token=api,
#     cache_dir=local_dir,
# )

# model = AutoModelForCausalLM.from_pretrained(
#     base_model,
#     use_auth_token=api,
#     torch_dtype=torch.float16,
#     cache_dir=local_dir,
#     device_map="auto",
# )

# hf_pipeline = pipeline(
#     "text-generation",
#     model=model,
#     tokenizer=tokenizer,
#     max_new_tokens=512,
#     do_sample=True,
#     temperature=0.7
# )

# hf_llm = HuggingFacePipeline(pipeline=hf_pipeline)