raushan-in commited on
Commit
66c0d0c
·
1 Parent(s): b99d8fb

file added

Browse files
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
- title: Dapa
3
- emoji: 🐢
4
- colorFrom: indigo
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: mit
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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__}")