Spaces:
No application file
No application file
raushan-in
commited on
Commit
·
66c0d0c
1
Parent(s):
b99d8fb
file added
Browse files- Dockerfile.client +14 -0
- Dockerfile.server +17 -0
- README.md +108 -8
- compose.yml +65 -0
- example.env +13 -0
- requirements_client.txt +4 -0
- requirements_server.txt +45 -0
- src/agent.py +61 -0
- src/auth.py +23 -0
- src/database.py +67 -0
- src/interface.py +88 -0
- src/main.py +23 -0
- src/prompts.py +56 -0
- src/routes.py +32 -0
- src/scams.py +51 -0
- src/schema.py +83 -0
- src/settings.py +66 -0
- src/tools.py +76 -0
- src/utils.py +79 -0
Dockerfile.client
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.12.3-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
COPY requirements_client.txt ./
|
6 |
+
RUN pip install --no-cache-dir -r requirements_client.txt
|
7 |
+
|
8 |
+
COPY . .
|
9 |
+
|
10 |
+
EXPOSE 8088
|
11 |
+
|
12 |
+
HEALTHCHECK CMD curl --fail http://localhost:8088/_stcore/health
|
13 |
+
|
14 |
+
ENTRYPOINT ["streamlit", "run", "src/interface.py", "--server.port=8088", "--server.address=0.0.0.0"]
|
Dockerfile.server
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.12.3-slim
|
2 |
+
|
3 |
+
# Set environment variables
|
4 |
+
ENV PYTHONUNBUFFERED 1
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
COPY requirements_server.txt ./
|
8 |
+
RUN pip install --no-cache-dir -r requirements_server.txt
|
9 |
+
|
10 |
+
# Copy the application code
|
11 |
+
COPY . .
|
12 |
+
|
13 |
+
# Expose the port
|
14 |
+
EXPOSE 8080
|
15 |
+
|
16 |
+
# Start the backend app
|
17 |
+
CMD ["python", "src/main.py"]
|
README.md
CHANGED
@@ -1,11 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
---
|
10 |
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# DAPA (Digital Arrest Protection App)
|
2 |
+
|
3 |
+
**Inspired by the Hindi word 'धप्पा' (meaning ‘busted’), A tool to combat digital scams.**
|
4 |
+
|
5 |
+
---
|
6 |
+
|
7 |
+
## Overview
|
8 |
+
DAPA enables users to report financial scams and frauds efficiently via a user-friendly platform. The system facilitates the following:
|
9 |
+
|
10 |
+
1. **Report a Scam**: Users can provide details such as the scammer’s phone number and a brief description of the fraud. DAPA categorizes the scam and records the report.
|
11 |
+
2. **Search a Phone Number**: Users can check whether a specific number has been previously flagged for fraudulent activities.
|
12 |
+
|
13 |
+
With its streamlined architecture powered by AI, DAPA aims to mitigate the growing epidemic of financial scams by creating a comprehensive record of scam incidents.
|
14 |
+
|
15 |
+
---
|
16 |
+
|
17 |
+
## Why DAPA Matters
|
18 |
+
### The Growing Threat of Financial Scams:
|
19 |
+
- **India**:
|
20 |
+
- According to reports, the annual financial loss due to digital scams and frauds in India amounts to **₹60,000 crore**.
|
21 |
+
- A surge in digital adoption has inadvertently made India a hotspot for cybercriminal activities, including the **Digital Arrest Scam** where scammers impersonate law enforcement officials to defraud victims of significant amounts.
|
22 |
+
|
23 |
+
- **Global**:
|
24 |
+
- Financial scams cost individuals and businesses **over $5 trillion annually worldwide**.
|
25 |
+
- An increasing number of scams now exploit vulnerabilities in mobile and social media platforms.
|
26 |
+
|
27 |
+
### Spotlight on the Digital Arrest Scam:
|
28 |
+
The Digital Arrest Scam, a notable threat in India, sees fraudsters pretending to be law enforcement officials to extort money by intimidation. Victims are coerced into paying under the pretense of avoiding legal trouble. (Sources: **The Hindu**)
|
29 |
+
|
30 |
+
Prime Minister Modi have highlighted the urgent need to address these issues through education and vigilance.
|
31 |
+
|
32 |
+
---
|
33 |
+
|
34 |
+
## Why an AI Chatbot Instead of Traditional Form-Based Tools?
|
35 |
+
DAPA leverages an AI chatbot over traditional form-based tools for the following reasons:
|
36 |
+
|
37 |
+
1. **Localized Language Support**:
|
38 |
+
- Users can narrate their experiences in their own local language, including dialects, through platforms like WhatsApp. The AI chatbot interprets, summarizes, and processes this information seamlessly.
|
39 |
+
|
40 |
+
2. **Ease of Use**:
|
41 |
+
- Unlike rigid forms, a conversational AI provides a more intuitive, human-like interface, enabling users to express their ordeal naturally without worrying about form structure or specific formats.
|
42 |
+
|
43 |
+
3. **Advanced Understanding**:
|
44 |
+
- The AI model not only captures and categorizes scam details but also creates concise summaries, ensuring that valuable information is accurately stored in a centralized database for combating scams.
|
45 |
+
|
46 |
+
4. **Centralized Scam Pattern Analysis**:
|
47 |
+
- The summaries generated by the chatbot help uncover common scam patterns. By analyzing these trends, DAPA contributes to developing preventive measures and combating scams more effectively at scale.
|
48 |
+
|
49 |
+
5. **Accessibility**:
|
50 |
+
- By integrating with popular platforms like WhatsApp, the AI chatbot increases accessibility, making it easier for users in remote or underserved areas to report scams.
|
51 |
+
|
52 |
+
6. **Insights**:
|
53 |
+
- With AI, identifying patterns and emerging threats faster than traditional tools, thereby enhancing preventative measures.
|
54 |
+
|
55 |
---
|
56 |
+
|
57 |
+
## Features
|
58 |
+
### Core Features:
|
59 |
+
- **Fraud Reporting**:
|
60 |
+
- Easy submission of scammer details.
|
61 |
+
- Categorization of the scam using AI-based inference.
|
62 |
+
- Centralized record maintenance for tracking scam patterns.
|
63 |
+
|
64 |
+
- **Fraud Search**:
|
65 |
+
- Quick lookup of phone numbers to check for prior scam reports.
|
66 |
+
|
67 |
+
### Technology Stack:
|
68 |
+
- **Backend**: Python, FastAPI, LangGraph
|
69 |
+
- **Frontend**: Streamlit
|
70 |
+
- **Database**: PostgreSQL
|
71 |
+
- **AI Model**: GROQ LLaMA 3 (Configurable)
|
72 |
+
|
73 |
---
|
74 |
|
75 |
+
## Setup Instructions
|
76 |
+
### Environment Configuration
|
77 |
+
Create the environment file:
|
78 |
+
```bash
|
79 |
+
cp example.env .env
|
80 |
+
```
|
81 |
+
- Add Groq token
|
82 |
+
|
83 |
+
### Build and Run the Application
|
84 |
+
Build the application container using Docker:
|
85 |
+
```bash
|
86 |
+
docker compose up --build -d
|
87 |
+
```
|
88 |
+
|
89 |
+
### Access the App:
|
90 |
+
- Chatbot: [http://localhost:8088/](http://localhost:8088/)
|
91 |
+
- API Documentation: [http://localhost:8080/docs](http://localhost:8080/docs)
|
92 |
+
|
93 |
+
---
|
94 |
+
|
95 |
+
## Architecture Diagram
|
96 |
+
![flow](https://github.com/user-attachments/assets/c51bd311-7b9b-4e9c-888b-190fc08e4da0)
|
97 |
+
|
98 |
+
---
|
99 |
+
|
100 |
+
## Future Roadmap
|
101 |
+
- **WhatsApp Integration**: Users will be able to report and identify scams via WhatsApp for enhanced convenience. This will be achieved through a webhook system.
|
102 |
+
- **Enhanced AI Capabilities**: Continuous improvement of the scam categorization model to ensure accuracy and adaptability to emerging scam types.
|
103 |
+
---
|
104 |
+
|
105 |
+
## Contact
|
106 |
+
For improvements or collaboration, feel free to connect:
|
107 |
+
[Raushan's LinkedIn Profile](https://www.linkedin.com/in/raushan-in/)
|
108 |
+
|
109 |
+
Together, let’s combat financial fraud and make the digital world safer for everyone.
|
110 |
+
|
111 |
+
**DAPA – Digital Arrest Protection App**
|
compose.yml
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
services:
|
2 |
+
dapa_be:
|
3 |
+
build:
|
4 |
+
context: .
|
5 |
+
dockerfile: Dockerfile.server
|
6 |
+
container_name: dapa_backend
|
7 |
+
depends_on:
|
8 |
+
- dapa_pg
|
9 |
+
ports:
|
10 |
+
- "8080:8080"
|
11 |
+
env_file:
|
12 |
+
- .env
|
13 |
+
environment:
|
14 |
+
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@dapa_pg:${POSTGRES_PORT}/${POSTGRES_DB}
|
15 |
+
networks:
|
16 |
+
- dapa-network
|
17 |
+
|
18 |
+
|
19 |
+
dapa_streamlit_fe:
|
20 |
+
build:
|
21 |
+
context: .
|
22 |
+
dockerfile: Dockerfile.client
|
23 |
+
container_name: dapa_streamlit
|
24 |
+
ports:
|
25 |
+
- "8088:8088"
|
26 |
+
depends_on:
|
27 |
+
- dapa_be
|
28 |
+
environment:
|
29 |
+
- BACKEND_URL=http://dapa_backend:8080
|
30 |
+
networks:
|
31 |
+
- dapa-network
|
32 |
+
develop:
|
33 |
+
watch:
|
34 |
+
- path: src/client/
|
35 |
+
action: sync+restart
|
36 |
+
target: /app/client/
|
37 |
+
- path: src/schema/
|
38 |
+
action: sync+restart
|
39 |
+
target: /app/schema/
|
40 |
+
- path: src/interface.py
|
41 |
+
action: sync+restart
|
42 |
+
target: /app/interface.py
|
43 |
+
|
44 |
+
dapa_pg:
|
45 |
+
image: postgres:latest
|
46 |
+
container_name: dapa_pg
|
47 |
+
ports:
|
48 |
+
- "5432:5432"
|
49 |
+
env_file:
|
50 |
+
- .env
|
51 |
+
networks:
|
52 |
+
- dapa-network
|
53 |
+
volumes:
|
54 |
+
- pg_db_1:/var/lib/postgresql/data
|
55 |
+
|
56 |
+
volumes:
|
57 |
+
pg_db_1:
|
58 |
+
|
59 |
+
networks:
|
60 |
+
dapa-network:
|
61 |
+
|
62 |
+
# To build and run the app:
|
63 |
+
# docker compose up --build -d
|
64 |
+
# or for dev easy
|
65 |
+
# docker compose watch
|
example.env
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MODE=dev
|
2 |
+
|
3 |
+
GROQ_API_KEY=
|
4 |
+
GROQ_MODEL=llama-3.3-70b-versatile
|
5 |
+
|
6 |
+
LANGCHAIN_TRACING_V2=false
|
7 |
+
LANGCHAIN_API_KEY=
|
8 |
+
|
9 |
+
# POSTGRES_DB
|
10 |
+
POSTGRES_USER=username
|
11 |
+
POSTGRES_PASSWORD=password
|
12 |
+
POSTGRES_DB=pgdb_name
|
13 |
+
POSTGRES_PORT=5432
|
requirements_client.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
httpx
|
2 |
+
pydantic
|
3 |
+
python-dotenv
|
4 |
+
streamlit
|
requirements_server.txt
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
annotated-types==0.7.0
|
2 |
+
anyio==4.8.0
|
3 |
+
asyncpg==0.30.0
|
4 |
+
certifi==2024.12.14
|
5 |
+
charset-normalizer==3.4.1
|
6 |
+
click==8.1.8
|
7 |
+
distro==1.9.0
|
8 |
+
fastapi==0.115.6
|
9 |
+
greenlet==3.1.1
|
10 |
+
groq==0.15.0
|
11 |
+
h11==0.14.0
|
12 |
+
httpcore==1.0.7
|
13 |
+
httpx==0.28.1
|
14 |
+
idna==3.10
|
15 |
+
jsonpatch==1.33
|
16 |
+
jsonpointer==3.0.0
|
17 |
+
langchain-core==0.3.29
|
18 |
+
langchain-groq==0.2.3
|
19 |
+
langgraph==0.2.62
|
20 |
+
langgraph-checkpoint==2.0.9
|
21 |
+
langgraph-sdk==0.1.51
|
22 |
+
langsmith==0.2.10
|
23 |
+
msgpack==1.1.0
|
24 |
+
numexpr==2.10.2
|
25 |
+
numpy==2.2.1
|
26 |
+
orjson==3.10.14
|
27 |
+
packaging==24.2
|
28 |
+
psycopg2-binary==2.9.10
|
29 |
+
pydantic==2.10.5
|
30 |
+
pydantic-settings==2.7.1
|
31 |
+
pydantic_core==2.27.2
|
32 |
+
python-dotenv==1.0.1
|
33 |
+
PyYAML==6.0.2
|
34 |
+
requests==2.32.3
|
35 |
+
requests-toolbelt==1.0.0
|
36 |
+
setuptools==69.5.1
|
37 |
+
sniffio==1.3.1
|
38 |
+
SQLAlchemy==2.0.37
|
39 |
+
sqlmodel==0.0.22
|
40 |
+
starlette==0.41.3
|
41 |
+
tenacity==9.0.0
|
42 |
+
typing_extensions==4.12.2
|
43 |
+
urllib3==2.3.0
|
44 |
+
uvicorn==0.34.0
|
45 |
+
wheel==0.43.0
|
src/agent.py
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""AI agents"""
|
2 |
+
|
3 |
+
from langchain_core.language_models.chat_models import BaseChatModel
|
4 |
+
from langchain_core.messages import AIMessage, SystemMessage
|
5 |
+
from langchain_core.runnables import (
|
6 |
+
RunnableConfig,
|
7 |
+
RunnableLambda,
|
8 |
+
RunnableSerializable,
|
9 |
+
)
|
10 |
+
from langchain_groq import ChatGroq
|
11 |
+
from langgraph.checkpoint.memory import MemorySaver
|
12 |
+
from langgraph.graph import END, MessagesState, StateGraph
|
13 |
+
from langgraph.prebuilt import ToolNode
|
14 |
+
|
15 |
+
from prompts import instructions
|
16 |
+
from settings import settings
|
17 |
+
from tools import register_scam, search_scam
|
18 |
+
|
19 |
+
llm = ChatGroq(
|
20 |
+
model=settings.GROQ_MODEL, temperature=settings.GROQ_MODEL_TEMP, streaming=False
|
21 |
+
)
|
22 |
+
|
23 |
+
|
24 |
+
tools = [register_scam, search_scam]
|
25 |
+
|
26 |
+
|
27 |
+
class AgentState(MessagesState, total=False):
|
28 |
+
"""`total=False` is PEP589 specs.
|
29 |
+
|
30 |
+
documentation: https://typing.readthedocs.io/en/latest/spec/typeddict.html#totality
|
31 |
+
"""
|
32 |
+
|
33 |
+
|
34 |
+
def wrap_model(model: BaseChatModel) -> RunnableSerializable[AgentState, AIMessage]:
|
35 |
+
model_with_tools = model.bind_tools(tools)
|
36 |
+
preprocessor = RunnableLambda(
|
37 |
+
lambda state: [SystemMessage(content=instructions)] + state["messages"],
|
38 |
+
name="StateModifier",
|
39 |
+
)
|
40 |
+
return preprocessor | model_with_tools
|
41 |
+
|
42 |
+
|
43 |
+
async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
|
44 |
+
model_runnable = wrap_model(llm)
|
45 |
+
response = await model_runnable.ainvoke(state, config)
|
46 |
+
# We return a list, because this will get added to the existing list
|
47 |
+
return {"messages": [response]}
|
48 |
+
|
49 |
+
|
50 |
+
# Define the graph
|
51 |
+
agent = StateGraph(AgentState)
|
52 |
+
agent.add_node("model", acall_model)
|
53 |
+
agent.add_node("tools", ToolNode(tools))
|
54 |
+
|
55 |
+
agent.set_entry_point("model")
|
56 |
+
|
57 |
+
# Add edges (transitions)
|
58 |
+
agent.add_edge("model", "tools")
|
59 |
+
agent.add_edge("tools", END)
|
60 |
+
|
61 |
+
cyber_guard = agent.compile(checkpointer=MemorySaver())
|
src/auth.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Annotated
|
2 |
+
|
3 |
+
from fastapi import Depends, HTTPException, status
|
4 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
5 |
+
|
6 |
+
from settings import settings
|
7 |
+
|
8 |
+
|
9 |
+
def verify_bearer(
|
10 |
+
http_auth: Annotated[
|
11 |
+
HTTPAuthorizationCredentials | None,
|
12 |
+
Depends(
|
13 |
+
HTTPBearer(
|
14 |
+
description="Please provide AUTH_SECRET api key.", auto_error=False
|
15 |
+
)
|
16 |
+
),
|
17 |
+
],
|
18 |
+
) -> None:
|
19 |
+
if not settings.AUTH_SECRET:
|
20 |
+
return
|
21 |
+
auth_secret = settings.AUTH_SECRET.get_secret_value()
|
22 |
+
if not http_auth or http_auth.credentials != auth_secret:
|
23 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
src/database.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
from contextlib import asynccontextmanager
|
3 |
+
from datetime import datetime
|
4 |
+
|
5 |
+
from fastapi import Depends
|
6 |
+
from pydantic import validator
|
7 |
+
from sqlalchemy.ext.asyncio import create_async_engine
|
8 |
+
from sqlalchemy.orm import sessionmaker
|
9 |
+
from sqlmodel import Field, SQLModel
|
10 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
11 |
+
|
12 |
+
from scams import scam_categories
|
13 |
+
from settings import settings
|
14 |
+
|
15 |
+
database_url = settings.DATABASE_URL.get_secret_value()
|
16 |
+
|
17 |
+
engine = create_async_engine(database_url, echo=settings.is_dev())
|
18 |
+
|
19 |
+
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
20 |
+
|
21 |
+
|
22 |
+
async def create_db_and_tables():
|
23 |
+
async with engine.begin() as conn:
|
24 |
+
await conn.run_sync(SQLModel.metadata.create_all)
|
25 |
+
|
26 |
+
|
27 |
+
@asynccontextmanager
|
28 |
+
async def get_session() -> AsyncSession:
|
29 |
+
"""
|
30 |
+
Dependency function to provide an async database session.
|
31 |
+
Ensures proper cleanup after use.
|
32 |
+
"""
|
33 |
+
async with async_session() as session:
|
34 |
+
try:
|
35 |
+
yield session
|
36 |
+
finally:
|
37 |
+
await session.close()
|
38 |
+
|
39 |
+
|
40 |
+
class Scammer(SQLModel, table=True):
|
41 |
+
"""Scammer ORM Model."""
|
42 |
+
|
43 |
+
id: int = Field(default=None, primary_key=True)
|
44 |
+
scammer_mobile: str = Field(index=True, description="Scammer mobile number")
|
45 |
+
scam_id: int = Field(description="Scam ID of the scam type")
|
46 |
+
reporter_ordeal: str = Field(description="Summary of the scam")
|
47 |
+
reporter_mobile: str = Field(description="Reporter mobile number")
|
48 |
+
created_at: datetime = Field(
|
49 |
+
default_factory=datetime.utcnow, description="Timestamp of report creation"
|
50 |
+
)
|
51 |
+
|
52 |
+
@validator("scammer_mobile", "reporter_mobile", pre=True)
|
53 |
+
def validate_mobile_number(cls, value: str) -> str:
|
54 |
+
"""Validate mobile numbers using a regex."""
|
55 |
+
pattern = r"^\+\d{1,3}-?\d{6,14}$" # E.164 format
|
56 |
+
if not re.match(pattern, value):
|
57 |
+
raise ValueError(f"Invalid mobile number: {value}")
|
58 |
+
return value
|
59 |
+
|
60 |
+
@validator("scam_id")
|
61 |
+
def validate_scam_id(cls, value: int) -> int:
|
62 |
+
"""Validate if scam_id exists in scam_categories."""
|
63 |
+
if value not in scam_categories.keys():
|
64 |
+
raise ValueError(
|
65 |
+
f"Invalid scam_id: {value}. Must be one of {list(scam_categories.keys())}."
|
66 |
+
)
|
67 |
+
return value
|
src/interface.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import os
|
3 |
+
|
4 |
+
import httpx
|
5 |
+
import streamlit as st
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
|
8 |
+
APP_TITLE = "DAPA"
|
9 |
+
APP_ICON = "🛡️"
|
10 |
+
|
11 |
+
# Load environment variables
|
12 |
+
load_dotenv()
|
13 |
+
BACKEND_URL = os.getenv("BACKEND_URL")
|
14 |
+
CHAT_API = BACKEND_URL + "/chat"
|
15 |
+
|
16 |
+
|
17 |
+
async def get_response(user_message: str, thread_id: str | None = None) -> dict:
|
18 |
+
payload = {"user_message": user_message, "thread_id": thread_id}
|
19 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
20 |
+
try:
|
21 |
+
response = await client.post(CHAT_API, json=payload)
|
22 |
+
response.raise_for_status()
|
23 |
+
return response.json()
|
24 |
+
except httpx.HTTPError as e:
|
25 |
+
return {"error": f"Error connecting to backend: {str(e)}"}
|
26 |
+
|
27 |
+
|
28 |
+
async def main():
|
29 |
+
st.set_page_config(page_title=APP_TITLE, page_icon=APP_ICON)
|
30 |
+
|
31 |
+
if "thread_id" not in st.session_state:
|
32 |
+
st.session_state.thread_id = None
|
33 |
+
if "messages" not in st.session_state:
|
34 |
+
st.session_state.messages = []
|
35 |
+
|
36 |
+
st.title(f"{APP_ICON} DAPA AI Assistant")
|
37 |
+
st.write(
|
38 |
+
"This bot helps you identify or report phone numbers involved in financial fraud or cyber scams."
|
39 |
+
"Please describe your incident below."
|
40 |
+
)
|
41 |
+
|
42 |
+
for message in st.session_state.messages:
|
43 |
+
responder_type = message["responder"]
|
44 |
+
if responder_type == "tool":
|
45 |
+
st.chat_message("tool", avatar="🛡️").write(message["content"])
|
46 |
+
else:
|
47 |
+
st.chat_message(responder_type).write(message["content"])
|
48 |
+
|
49 |
+
# User input
|
50 |
+
if user_input := st.chat_input("Type your message here..."):
|
51 |
+
st.session_state.messages.append({"responder": "human", "content": user_input})
|
52 |
+
st.chat_message("human").write(user_input)
|
53 |
+
|
54 |
+
response = await get_response(user_input, st.session_state.thread_id)
|
55 |
+
|
56 |
+
if "error" in response:
|
57 |
+
st.error(response["error"])
|
58 |
+
else:
|
59 |
+
response_message = response["response_message"]
|
60 |
+
responder = response["responder"]
|
61 |
+
st.session_state.thread_id = response["thread_id"]
|
62 |
+
|
63 |
+
# Append the response to session state
|
64 |
+
st.session_state.messages.append(
|
65 |
+
{"responder": responder, "content": response_message}
|
66 |
+
)
|
67 |
+
if responder == "tool":
|
68 |
+
st.chat_message("tool", avatar="🛡️").write(response_message)
|
69 |
+
else:
|
70 |
+
st.chat_message(responder).write(response_message)
|
71 |
+
|
72 |
+
with st.sidebar:
|
73 |
+
st.header(f"{APP_ICON} {APP_TITLE}")
|
74 |
+
st.write("DAPA chatbot for secure reporting of scams.")
|
75 |
+
|
76 |
+
# Privacy Section
|
77 |
+
with st.expander("🔒 Privacy"):
|
78 |
+
st.write(
|
79 |
+
"Query and response in this app are anonymously recorded and saved to LangSmith for product evaluation and improvement purposes."
|
80 |
+
)
|
81 |
+
|
82 |
+
st.markdown(
|
83 |
+
"Made with ❤️ by [Raushan](https://www.linkedin.com/in/raushan-in/) in Trier"
|
84 |
+
)
|
85 |
+
|
86 |
+
|
87 |
+
if __name__ == "__main__":
|
88 |
+
asyncio.run(main())
|
src/main.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import uvicorn
|
2 |
+
from fastapi import FastAPI
|
3 |
+
|
4 |
+
from database import create_db_and_tables
|
5 |
+
from routes import bot_router
|
6 |
+
from settings import settings
|
7 |
+
|
8 |
+
|
9 |
+
async def lifespan(app):
|
10 |
+
await create_db_and_tables()
|
11 |
+
yield
|
12 |
+
|
13 |
+
|
14 |
+
app = FastAPI(title="DAPA", summary="Digital Arrest Protection App", lifespan=lifespan)
|
15 |
+
|
16 |
+
# Endpoint router
|
17 |
+
app.include_router(bot_router)
|
18 |
+
|
19 |
+
|
20 |
+
if __name__ == "__main__":
|
21 |
+
uvicorn.run(
|
22 |
+
"main:app", host=settings.HOST, port=settings.PORT, reload=settings.is_dev()
|
23 |
+
)
|
src/prompts.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from scams import scam_categories_str
|
2 |
+
|
3 |
+
instructions = f"""
|
4 |
+
You are an AI bot named DAPA. Your job is to assist users in reporting potential digital financial scams or fraud via mobile communication.
|
5 |
+
DAPA stands for Digital Arrest Protection App.
|
6 |
+
|
7 |
+
# Follow these instructions strictly:
|
8 |
+
- Concise Responses: Keep your replies clear and short.
|
9 |
+
- Only register a scam if the description indicates financial fraud or money involved; otherwise, guide the user to report the issue to appropriate authorities.
|
10 |
+
- Language Adaptability: Respond in the user’s preferred language but use English for tool inputs.
|
11 |
+
- Validate Inputs: Collect all required details from the user before using any tool.
|
12 |
+
- Scammer’s Mobile Number: Must be in +XX-<mobile_number> format.
|
13 |
+
- Scam Type: Identify Scam Type from reporter’s ordeal. Show the scam name (e.g., "Fake Job Scam") to the user, but pass the corresponding ID (e.g., 9) to the tool.
|
14 |
+
- Display Scam Name only instead of Scam ID for human user understanding.
|
15 |
+
- Reporter’s Mobile Number: Must also be in +XX-<mobile_number> format.
|
16 |
+
- Only respond to cases involving cyber scams that are financial in nature and connected to a mobile number.
|
17 |
+
- Avoid assisting with unrelated queries (e.g., general protection tips, general knowledge, mathematical, language or programming questions).
|
18 |
+
- Confirm Before Registering: Always confirm the scammer’s mobile number before registering. Register only if the user explicitly agrees.
|
19 |
+
- If the user provide 0 as the country code for the scammer's mobile number, even after explicitly being asked, use the reporter's country code as a fallback.
|
20 |
+
- Prioritize Scammer Search When Only Mobile Number is Provided.
|
21 |
+
- Before searching for a scammer's mobile number, format it into the standard format with country code (+XX-<mobile_number>).
|
22 |
+
- Keep responses concise and ask for one piece of information at a time to avoid overwhelming the user.
|
23 |
+
- Validate that all required details (scammer's number, description, and reporter's number) are provided before attempting to register the report.
|
24 |
+
- If the country code is not provided, do not make assumptions. Always ask the user to provide.
|
25 |
+
- In case of a ValueError when using the tool, Correct the parameters or missing value and try again.
|
26 |
+
|
27 |
+
Tool Usage: Use tools only after collecting and validating all inputs.
|
28 |
+
Pass scam ID to the tool but show scam name to the user for clarity.
|
29 |
+
Use the Register Scam tool only after explicit user confirmation.
|
30 |
+
Use the Search Scam tool only if the user requests to check a specific number.
|
31 |
+
|
32 |
+
# Predefined Scam Categories: Only use the following scam types:
|
33 |
+
|
34 |
+
{scam_categories_str}
|
35 |
+
|
36 |
+
# Example Scenarios:
|
37 |
+
|
38 |
+
1. Reporting a Scam
|
39 |
+
User: Hi.
|
40 |
+
DAPA: Hi there! 😊 I’m here to assist you.
|
41 |
+
|
42 |
+
I can help you in two ways:
|
43 |
+
|
44 |
+
1. **Report a Scam:** Please provide the following details:
|
45 |
+
- Scammer’s mobile number (with country code).
|
46 |
+
- A brief description of your experience (up to 50 words).
|
47 |
+
- Your mobile number.
|
48 |
+
|
49 |
+
2. **Identify a Suspicious Number:**
|
50 |
+
Provide the mobile number and type "search". I’ll check if the number has been reported before.
|
51 |
+
|
52 |
+
2. What is Digital Arrest?
|
53 |
+
DAPA: A digital arrest scam is an online scam that defrauds victims of their hard-earned money.
|
54 |
+
The scammers intimidate the victims and falsely accuse them of illegal activities.
|
55 |
+
They later demand money and puts them under pressure for making the payment.
|
56 |
+
"""
|
src/routes.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Endpoints defination
|
3 |
+
"""
|
4 |
+
|
5 |
+
from fastapi import APIRouter, Depends, HTTPException
|
6 |
+
|
7 |
+
from auth import verify_bearer
|
8 |
+
from schema import SingleResponse, UserInput
|
9 |
+
from utils import get_llm_response, infer_chat_message
|
10 |
+
|
11 |
+
bot_router = APIRouter(tags=["bot"], dependencies=[Depends(verify_bearer)])
|
12 |
+
|
13 |
+
|
14 |
+
@bot_router.post("/chat")
|
15 |
+
async def invoke(user_input: UserInput) -> SingleResponse:
|
16 |
+
"""
|
17 |
+
Invoke an agent with user input to retrieve a final response.
|
18 |
+
|
19 |
+
Use thread_id to persist and continue a multi-turn conversation.
|
20 |
+
"""
|
21 |
+
try:
|
22 |
+
response, thread_id = await get_llm_response(user_input)
|
23 |
+
last_message = infer_chat_message(response["messages"][-1])
|
24 |
+
response = {
|
25 |
+
"response_message": last_message.content,
|
26 |
+
"responder": last_message.type,
|
27 |
+
"thread_id": thread_id,
|
28 |
+
}
|
29 |
+
return SingleResponse(**response)
|
30 |
+
except Exception as e:
|
31 |
+
print(e)
|
32 |
+
raise HTTPException(status_code=500, detail="Unexpected error")
|
src/scams.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
scam_categories = {
|
2 |
+
1: [
|
3 |
+
"Fake Authority Call",
|
4 |
+
"Scammers impersonating law enforcement officials (e.g., CBI, customs, police) or service agents, coercing victims into making payments or money.",
|
5 |
+
],
|
6 |
+
2: [
|
7 |
+
"Service Disconnection Scam",
|
8 |
+
"Threats of service disconnection unless immediate verification or payment is made.",
|
9 |
+
],
|
10 |
+
3: [
|
11 |
+
"UPI Scam",
|
12 |
+
"Scammers claim accidental UPI payments and request refunds, or attempt scams related to UPI, PhonePe, Google Pay, or any quick payment interface.",
|
13 |
+
],
|
14 |
+
4: ["OTP Scam", "Scammers request OTPs to gain unauthorized access to accounts."],
|
15 |
+
5: [
|
16 |
+
"Fake Buyer/Seller Scam",
|
17 |
+
"Scammers pose as buyers requesting refunds or as fraudulent sellers asking for advance payments.",
|
18 |
+
],
|
19 |
+
6: [
|
20 |
+
"Phishing or Link Scam",
|
21 |
+
"Fraudulent SMS or calls designed to gain unauthorized access to banking platforms by sending malicious links or asking for sensitive details.",
|
22 |
+
],
|
23 |
+
7: [
|
24 |
+
"Video Call Scam",
|
25 |
+
"Blackmail involving compromising video calls, or screenshots, used to demand money.",
|
26 |
+
],
|
27 |
+
8: [
|
28 |
+
"Fake Bank Staff Scam",
|
29 |
+
"Calls from scammers posing as bank officials, requesting sensitive banking details.",
|
30 |
+
],
|
31 |
+
9: [
|
32 |
+
"Fake Job Scam",
|
33 |
+
"Scammers posing as recruiters, demanding service or registration fees for fake job offers.",
|
34 |
+
],
|
35 |
+
10: [
|
36 |
+
"Lottery Scam",
|
37 |
+
"Messages claiming lottery wins, lucky draws, or prizes, and requesting fees for processing.",
|
38 |
+
],
|
39 |
+
11: [
|
40 |
+
"Fake Identity Scam",
|
41 |
+
"Scammers imitate known individuals and request money transfers.",
|
42 |
+
],
|
43 |
+
12: [
|
44 |
+
"Other Cyber Scam",
|
45 |
+
"Any other scam conducted via phone that involves monetary fraud.",
|
46 |
+
],
|
47 |
+
}
|
48 |
+
|
49 |
+
scam_categories_str = "\n".join(
|
50 |
+
[f"{k}: {v[0]}-{v[1]}" for k, v in scam_categories.items()]
|
51 |
+
)
|
src/schema.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Any, Literal, NotRequired
|
2 |
+
|
3 |
+
from pydantic import BaseModel, Field
|
4 |
+
from typing_extensions import TypedDict
|
5 |
+
|
6 |
+
|
7 |
+
class UserInput(BaseModel):
|
8 |
+
"""Basic user input for the agent."""
|
9 |
+
|
10 |
+
user_message: str = Field(
|
11 |
+
description="User message to the AI agent.",
|
12 |
+
examples=["Hello, I want to report."],
|
13 |
+
)
|
14 |
+
thread_id: str | None = Field(
|
15 |
+
description="Thread ID to persist and continue a multi-turn conversation.",
|
16 |
+
default=None,
|
17 |
+
examples=["847c6285-8fc9-4560-a83f-4e628xx09254"],
|
18 |
+
)
|
19 |
+
|
20 |
+
|
21 |
+
class SingleResponse(BaseModel):
|
22 |
+
"""Basic user input for the agent."""
|
23 |
+
|
24 |
+
response_message: str = Field(
|
25 |
+
description="Response content based on user input.",
|
26 |
+
examples=["Hello, I am a bot."],
|
27 |
+
)
|
28 |
+
responder: Literal["human", "ai", "tool", "custom"] = Field(
|
29 |
+
description="Generator of response.",
|
30 |
+
examples=["human", "ai", "tool", "custom"],
|
31 |
+
)
|
32 |
+
thread_id: str | None = Field(
|
33 |
+
description="Thread ID to persist and continue a multi-turn conversation.",
|
34 |
+
default=None,
|
35 |
+
examples=["847c6285-8fc9-4560-a83f-4e628xx09254"],
|
36 |
+
)
|
37 |
+
|
38 |
+
|
39 |
+
class ToolCall(TypedDict):
|
40 |
+
"""Represents a request to call a tool."""
|
41 |
+
|
42 |
+
name: str
|
43 |
+
"""The name of the tool to be called."""
|
44 |
+
args: dict[str, Any]
|
45 |
+
"""The arguments to the tool call."""
|
46 |
+
id: str | None
|
47 |
+
"""An identifier associated with the tool call."""
|
48 |
+
type: NotRequired[Literal["tool_call"]]
|
49 |
+
|
50 |
+
|
51 |
+
class Chat(BaseModel):
|
52 |
+
"""Message in a chat."""
|
53 |
+
|
54 |
+
type: Literal["human", "ai", "tool", "custom"] = Field(
|
55 |
+
description="Role of the message.",
|
56 |
+
examples=["human", "ai", "tool", "custom"],
|
57 |
+
)
|
58 |
+
content: str = Field(
|
59 |
+
description="Content of the message.",
|
60 |
+
examples=["Hello, world!"],
|
61 |
+
)
|
62 |
+
tool_calls: list[ToolCall] = Field(
|
63 |
+
description="Tool calls in the message.",
|
64 |
+
default=[],
|
65 |
+
)
|
66 |
+
tool_call_id: str | None = Field(
|
67 |
+
description="Tool call that this message is responding to.",
|
68 |
+
default=None,
|
69 |
+
examples=["call_Jja7J89XsjrOLA5r!MEOW!SL"],
|
70 |
+
)
|
71 |
+
run_id: str | None = Field(
|
72 |
+
description="Run ID of the message.",
|
73 |
+
default=None,
|
74 |
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
75 |
+
)
|
76 |
+
response_metadata: dict[str, Any] = Field(
|
77 |
+
description="Response metadata. For example: response headers, logprobs, token counts.",
|
78 |
+
default={},
|
79 |
+
)
|
80 |
+
custom_data: dict[str, Any] = Field(
|
81 |
+
description="Custom message data.",
|
82 |
+
default={},
|
83 |
+
)
|
src/settings.py
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Any
|
2 |
+
|
3 |
+
from dotenv import find_dotenv
|
4 |
+
from pydantic import SecretStr, computed_field
|
5 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
6 |
+
|
7 |
+
|
8 |
+
class Settings(BaseSettings):
|
9 |
+
"""
|
10 |
+
Environment specific configuration
|
11 |
+
"""
|
12 |
+
|
13 |
+
model_config = SettingsConfigDict(
|
14 |
+
env_file=find_dotenv(),
|
15 |
+
env_file_encoding="utf-8",
|
16 |
+
env_ignore_empty=True,
|
17 |
+
extra="ignore",
|
18 |
+
validate_default=False,
|
19 |
+
)
|
20 |
+
# path
|
21 |
+
MODE: str | None = None # dev, prod
|
22 |
+
HOST: str = "0.0.0.0"
|
23 |
+
PORT: int = 8080
|
24 |
+
|
25 |
+
# Secret keys
|
26 |
+
AUTH_SECRET: SecretStr | None = None
|
27 |
+
JWT_ALGORITHM: str = "HS256"
|
28 |
+
|
29 |
+
# LLM API keys
|
30 |
+
GROQ_API_KEY: SecretStr
|
31 |
+
GROQ_MODEL: str = "llama3-8b-8192"
|
32 |
+
GROQ_MODEL_TEMP: float = 0.5
|
33 |
+
|
34 |
+
# Tracing for Langchain
|
35 |
+
LANGCHAIN_TRACING_V2: bool = (
|
36 |
+
False # Data will be sent to Langchain for monitering and improvement, If enabled.
|
37 |
+
)
|
38 |
+
LANGCHAIN_PROJECT: str = "default"
|
39 |
+
LANGCHAIN_ENDPOINT: str = "https://api.smith.langchain.com"
|
40 |
+
LANGCHAIN_API_KEY: SecretStr | None = None # LangSmith API key
|
41 |
+
|
42 |
+
# DB
|
43 |
+
DATABASE_URL: SecretStr
|
44 |
+
|
45 |
+
def model_post_init(self, __context: Any) -> None:
|
46 |
+
"""
|
47 |
+
Validate the settings after initialization
|
48 |
+
"""
|
49 |
+
if self.LANGCHAIN_TRACING_V2 and self.LANGCHAIN_API_KEY is None:
|
50 |
+
raise ValueError("Tracing is enabled, but LANGCHAIN_API_KEY is missing!")
|
51 |
+
|
52 |
+
if self.GROQ_API_KEY is None:
|
53 |
+
raise ValueError(
|
54 |
+
"GROQ_API_KEY is required! This key enables the application to connect with an advanced language model that understands queries and provides intelligent responses. You can generate your API at https://console.groq.com/keys ."
|
55 |
+
)
|
56 |
+
|
57 |
+
@computed_field
|
58 |
+
@property
|
59 |
+
def BASE_URL(self) -> str:
|
60 |
+
return f"http://{self.HOST}:{self.PORT}"
|
61 |
+
|
62 |
+
def is_dev(self) -> bool:
|
63 |
+
return self.MODE == "dev"
|
64 |
+
|
65 |
+
|
66 |
+
settings = Settings()
|
src/tools.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_core.tools import tool
|
2 |
+
from sqlmodel import select
|
3 |
+
|
4 |
+
from database import Scammer, get_session
|
5 |
+
|
6 |
+
|
7 |
+
@tool
|
8 |
+
async def register_scam(
|
9 |
+
scammer_mobile: str, scam_id: int, reporter_ordeal: str, reporter_mobile: str
|
10 |
+
) -> str:
|
11 |
+
"""
|
12 |
+
Registers a report of a scam incident into the database.
|
13 |
+
|
14 |
+
Parameters:
|
15 |
+
- scammer_mobile (str): The mobile_number of the alleged scammer.
|
16 |
+
Must be formatted as "+XX-<mobile_number>", where "+XX" is the country code.
|
17 |
+
- scam_id (int): The unique identifier for the type of scam.
|
18 |
+
- reporter_ordeal (str): A summary of the ordeal narrated by the reporter.
|
19 |
+
Should not exceed 50 words.
|
20 |
+
- reporter_mobile (str): The mobile_number of the person reporting the scam.
|
21 |
+
Must be formatted as "+XX-<mobile_number>", where "+XX" is the country code.
|
22 |
+
|
23 |
+
Returns:
|
24 |
+
str: A confirmation message if the report is registered successfully, or an error
|
25 |
+
message if an exception occurs during the registration process.
|
26 |
+
"""
|
27 |
+
try:
|
28 |
+
scammer = Scammer(
|
29 |
+
scammer_mobile=scammer_mobile,
|
30 |
+
scam_id=scam_id,
|
31 |
+
reporter_ordeal=reporter_ordeal,
|
32 |
+
reporter_mobile=reporter_mobile,
|
33 |
+
)
|
34 |
+
async with get_session() as session:
|
35 |
+
session.add(scammer)
|
36 |
+
await session.commit()
|
37 |
+
return f"{scammer_mobile} has been registered as a scammer. ✅ Thank you for combating scams! 🥇"
|
38 |
+
except ValueError as e:
|
39 |
+
print(repr(exc))
|
40 |
+
return f"ValueError: {repr(exc)}"
|
41 |
+
except Exception as exc:
|
42 |
+
print(repr(exc))
|
43 |
+
return f"An error occurred in registering a report for {scammer_mobile}."
|
44 |
+
|
45 |
+
|
46 |
+
register_scam.name = "Register Scam"
|
47 |
+
|
48 |
+
|
49 |
+
@tool
|
50 |
+
async def search_scam(scammer_mobile: str) -> str:
|
51 |
+
"""
|
52 |
+
Searches the database for scam reports associated with the provided mobile number.
|
53 |
+
|
54 |
+
Parameters:
|
55 |
+
scammer_mobile (str): The mobile number of the alleged scammer, formatted as "+XX-<mobile_number>",
|
56 |
+
where "+XX" is the country code.
|
57 |
+
|
58 |
+
Returns:
|
59 |
+
str: If a scam report is found, returns a string representation of the scam count.
|
60 |
+
If no scams are found, returns a message indicating that the mobile number is not reported.
|
61 |
+
If an error occurs during the search process, returns an error message.
|
62 |
+
"""
|
63 |
+
try:
|
64 |
+
async with get_session() as session:
|
65 |
+
statement = select(Scammer).where(Scammer.scammer_mobile == scammer_mobile)
|
66 |
+
result = await session.exec(statement)
|
67 |
+
scams = result.all()
|
68 |
+
if not scams:
|
69 |
+
return f"{scammer_mobile} has never been reported for scams or fraudulent activity."
|
70 |
+
return f"{scammer_mobile} has been reported as a scammer {len(scams)} times in the past. 🚨 Be alert! ⚠️"
|
71 |
+
except Exception as exc:
|
72 |
+
print(repr(exc))
|
73 |
+
return f"An error occurred while searching scam for {scammer_mobile}."
|
74 |
+
|
75 |
+
|
76 |
+
search_scam.name = "Search Scam"
|
src/utils.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from uuid import uuid4
|
2 |
+
|
3 |
+
from langchain_core.messages import (
|
4 |
+
AIMessage,
|
5 |
+
BaseMessage,
|
6 |
+
ChatMessage,
|
7 |
+
HumanMessage,
|
8 |
+
ToolMessage,
|
9 |
+
)
|
10 |
+
from langchain_core.runnables import RunnableConfig
|
11 |
+
|
12 |
+
from agent import cyber_guard
|
13 |
+
from schema import Chat
|
14 |
+
from settings import settings
|
15 |
+
|
16 |
+
|
17 |
+
async def get_llm_response(user_input):
|
18 |
+
thread_id = user_input.thread_id or str(uuid4())
|
19 |
+
kwargs = {
|
20 |
+
"input": {"messages": [HumanMessage(content=user_input.user_message)]},
|
21 |
+
"config": RunnableConfig(
|
22 |
+
configurable={"thread_id": thread_id, "model": settings.GROQ_MODEL}
|
23 |
+
),
|
24 |
+
}
|
25 |
+
response = await cyber_guard.ainvoke(**kwargs)
|
26 |
+
return response, thread_id
|
27 |
+
|
28 |
+
|
29 |
+
def convert_message_content_to_string(content: str | list[str | dict]) -> str:
|
30 |
+
if isinstance(content, str):
|
31 |
+
return content
|
32 |
+
text: list[str] = []
|
33 |
+
for content_item in content:
|
34 |
+
if isinstance(content_item, str):
|
35 |
+
text.append(content_item)
|
36 |
+
continue
|
37 |
+
if content_item["type"] == "text":
|
38 |
+
text.append(content_item["text"])
|
39 |
+
return "".join(text)
|
40 |
+
|
41 |
+
|
42 |
+
def infer_chat_message(message: BaseMessage) -> Chat:
|
43 |
+
"""Create a Chat from a LangChain message."""
|
44 |
+
match message:
|
45 |
+
case HumanMessage():
|
46 |
+
human_message = Chat(
|
47 |
+
type="human",
|
48 |
+
content=convert_message_content_to_string(message.content),
|
49 |
+
)
|
50 |
+
return human_message
|
51 |
+
case AIMessage():
|
52 |
+
ai_message = Chat(
|
53 |
+
type="ai",
|
54 |
+
content=convert_message_content_to_string(message.content),
|
55 |
+
)
|
56 |
+
if message.tool_calls:
|
57 |
+
ai_message.tool_calls = message.tool_calls
|
58 |
+
if message.response_metadata:
|
59 |
+
ai_message.response_metadata = message.response_metadata
|
60 |
+
return ai_message
|
61 |
+
case ToolMessage():
|
62 |
+
tool_message = Chat(
|
63 |
+
type="tool",
|
64 |
+
content=convert_message_content_to_string(message.content),
|
65 |
+
tool_call_id=message.tool_call_id,
|
66 |
+
)
|
67 |
+
return tool_message
|
68 |
+
case ChatMessage():
|
69 |
+
if message.role == "custom":
|
70 |
+
custom_message = Chat(
|
71 |
+
type="custom",
|
72 |
+
content="",
|
73 |
+
custom_data=message.content[0],
|
74 |
+
)
|
75 |
+
return custom_message
|
76 |
+
else:
|
77 |
+
raise ValueError(f"Unsupported chat message role: {message.role}")
|
78 |
+
case _:
|
79 |
+
raise ValueError(f"Unsupported message type: {message.__class__.__name__}")
|