ariansyahdedy commited on
Commit
7b2511b
·
1 Parent(s): a09e48d

Add memory

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
app/handlers/message_handler.py CHANGED
@@ -8,6 +8,8 @@ from app.services.download_media import download_whatsapp_media
8
  from app.services.message import process_message_with_llm
9
  from app.models.message_types import Message, MediaType, MediaContent
10
 
 
 
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
@@ -26,7 +28,7 @@ class MessageHandler:
26
  self.media_handler = media_handler
27
  self.logger = logger
28
 
29
- async def handle(self, raw_message: dict, whatsapp_token: str, whatsapp_url:str,gemini_api:str, rag_system:Any = None) -> dict:
30
  try:
31
  # Parse message
32
  message = MessageParser.parse(raw_message)
@@ -38,24 +40,35 @@ class MessageHandler:
38
  # Download media
39
  media_paths = await self._process_media(message, whatsapp_token)
40
 
41
- self.chat_manager.initialize_chat(message.sender_id)
 
 
 
42
 
 
43
 
 
 
44
  # Process message with LLM
45
  result = await process_message_with_llm(
46
  message.sender_id,
47
  message.content,
48
- self.chat_manager.get_chat_history(message.sender_id),
 
49
  rag_system = rag_system,
 
 
50
  whatsapp_token=whatsapp_token,
51
  whatsapp_url=whatsapp_url,
 
52
  **media_paths
53
  )
54
  self.logger.info(f"Result: {result}")
55
  # Append message to chat to keep track of conversation
56
- self.chat_manager.append_message(message.sender_id, "user", message.content)
57
- self.chat_manager.append_message(message.sender_id, "model", result)
58
 
 
59
  self.message_cache.add(message.id)
60
 
61
  return {"status": "success", "message_id": message.id, "result": result}
 
8
  from app.services.message import process_message_with_llm
9
  from app.models.message_types import Message, MediaType, MediaContent
10
 
11
+ from app.memory import AgentMemory
12
+
13
  import logging
14
 
15
  logger = logging.getLogger(__name__)
 
28
  self.media_handler = media_handler
29
  self.logger = logger
30
 
31
+ async def handle(self, raw_message: dict, whatsapp_token: str, whatsapp_url:str,gemini_api:str, rag_system:Any = None, agentMemory:Any=None, memory:Any=None) -> dict:
32
  try:
33
  # Parse message
34
  message = MessageParser.parse(raw_message)
 
40
  # Download media
41
  media_paths = await self._process_media(message, whatsapp_token)
42
 
43
+ # Simple class to store chat temporarily
44
+ # self.chat_manager.initialize_chat(message.sender_id)
45
+
46
+ user = await agentMemory.add_user(message.sender_id, message.sender_id)
47
 
48
+ await memory.add_message(message.sender_id, "user", message.content)
49
 
50
+ history = await memory.get_history(message.sender_id, last_n = 2)
51
+ print(f"chat_history: {history }")
52
  # Process message with LLM
53
  result = await process_message_with_llm(
54
  message.sender_id,
55
  message.content,
56
+ # self.chat_manager.get_chat_history(message.sender_id),
57
+ history,
58
  rag_system = rag_system,
59
+ agentMemory=agentMemory,
60
+ memory = memory,
61
  whatsapp_token=whatsapp_token,
62
  whatsapp_url=whatsapp_url,
63
+
64
  **media_paths
65
  )
66
  self.logger.info(f"Result: {result}")
67
  # Append message to chat to keep track of conversation
68
+ # self.chat_manager.append_message(message.sender_id, "user", message.content)
69
+ # self.chat_manager.append_message(message.sender_id, "model", result)
70
 
71
+ await memory.add_message(message.sender_id, "model", result)
72
  self.message_cache.add(message.id)
73
 
74
  return {"status": "success", "message_id": message.id, "result": result}
app/handlers/webhook_handler.py CHANGED
@@ -18,7 +18,7 @@ class WebhookHandler:
18
  self.message_handler = message_handler
19
  self.logger = logging.getLogger(__name__)
20
 
21
- async def process_webhook(self, payload: dict, whatsapp_token: str, whatsapp_url:str,gemini_api:str, rag_system:Any = None) -> WebhookResponse:
22
  request_id = f"req_{int(time.time()*1000)}"
23
  results = []
24
 
@@ -43,6 +43,8 @@ class WebhookHandler:
43
  whatsapp_url=whatsapp_url,
44
  gemini_api=gemini_api,
45
  rag_system=rag_system,
 
 
46
  )
47
  results.append(response)
48
 
 
18
  self.message_handler = message_handler
19
  self.logger = logging.getLogger(__name__)
20
 
21
+ async def process_webhook(self, payload: dict, whatsapp_token: str, whatsapp_url:str,gemini_api:str, rag_system:Any = None, agentMemory:Any = None, memory:Any = None) -> WebhookResponse:
22
  request_id = f"req_{int(time.time()*1000)}"
23
  results = []
24
 
 
43
  whatsapp_url=whatsapp_url,
44
  gemini_api=gemini_api,
45
  rag_system=rag_system,
46
+ agentMemory = agentMemory,
47
+ memory = memory
48
  )
49
  results.append(response)
50
 
app/main.py CHANGED
@@ -27,8 +27,11 @@ from app.services.chat_manager import ChatManager
27
  from app.api.api_prompt import prompt_router
28
  from app.api.api_file import file_router, load_file_with_markdown_function
29
  from app.utils.load_env import ACCESS_TOKEN, WHATSAPP_API_URL, GEMINI_API
 
 
30
 
31
-
 
32
  from markitdown import MarkItDown
33
 
34
  # Configure logging
@@ -64,31 +67,38 @@ async def setup_message_handler():
64
  media_handler=media_handler,
65
  logger=logger
66
  )
67
- async def setup_rag_system():
68
- embedding_model = SentenceTransformer('all-MiniLM-L6-v2') # Replace with your model if different
69
- rag_system = RAGSystem(embedding_model)
70
 
71
 
72
- return rag_system
73
  # Initialize FastAPI app
74
  @asynccontextmanager
75
  async def lifespan(app: FastAPI):
76
 
77
 
78
  try:
 
 
 
 
79
  # await init_db()
80
-
81
  logger.info("Connected to the MongoDB database!")
82
- rag_system = await setup_rag_system()
 
83
 
84
- app.state.rag_system = rag_system
 
 
85
 
86
  global message_handler, webhook_handler
87
  message_handler = await setup_message_handler()
88
  webhook_handler = WebhookHandler(message_handler)
89
  # collections = app.database.list_collection_names()
90
  # print(f"Collections in {db_name}: {collections}")
91
- await load_file_with_markdown_function(rag_system=rag_system, filepaths=indexed_links)
92
  yield
93
  except Exception as e:
94
  logger.error(e)
@@ -96,6 +106,8 @@ async def lifespan(app: FastAPI):
96
  # Initialize Limiter and Prometheus Metrics
97
  limiter = Limiter(key_func=get_remote_address)
98
  app = FastAPI(lifespan=lifespan)
 
 
99
  app.state.limiter = limiter
100
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
101
 
@@ -109,6 +121,27 @@ app.include_router(file_router, prefix="/file_load", tags=["File Load"])
109
  webhook_requests = Counter('webhook_requests_total', 'Total webhook requests')
110
  webhook_processing_time = Histogram('webhook_processing_seconds', 'Time spent processing webhook')
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  # Start Prometheus metrics server on port 8002
113
  # start_http_server(8002)
114
  # Register webhook routes
@@ -124,6 +157,8 @@ async def webhook(request: Request, background_tasks: BackgroundTasks):
124
  payload = await request.json()
125
 
126
  rag_system = request.app.state.rag_system
 
 
127
  # validated_payload = WebhookPayload(**payload) # Validate payload
128
  # logger.info(f"Validated Payload: {validated_payload}")
129
 
@@ -159,6 +194,8 @@ async def webhook(request: Request, background_tasks: BackgroundTasks):
159
  whatsapp_url=WHATSAPP_API_URL,
160
  gemini_api=GEMINI_API,
161
  rag_system=rag_system,
 
 
162
  )
163
  # Return HTTP 200 immediately
164
  return JSONResponse(
 
27
  from app.api.api_prompt import prompt_router
28
  from app.api.api_file import file_router, load_file_with_markdown_function
29
  from app.utils.load_env import ACCESS_TOKEN, WHATSAPP_API_URL, GEMINI_API
30
+ from fastapi.staticfiles import StaticFiles
31
+ from vidavox.core import RAG_Engine
32
 
33
+ from app.memory import AgentMemory
34
+ from app.settings import settings
35
  from markitdown import MarkItDown
36
 
37
  # Configure logging
 
67
  media_handler=media_handler,
68
  logger=logger
69
  )
70
+ # async def setup_rag_system():
71
+ # embedding_model = SentenceTransformer('all-MiniLM-L6-v2') # Replace with your model if different
72
+ # rag_system = RAGSystem(embedding_model)
73
 
74
 
75
+ # return rag_system
76
  # Initialize FastAPI app
77
  @asynccontextmanager
78
  async def lifespan(app: FastAPI):
79
 
80
 
81
  try:
82
+
83
+ agentMemory = AgentMemory(db_url=settings.POSTGRES_DB_URL)
84
+
85
+ memory = await agentMemory.initialize()
86
  # await init_db()
87
+ file_paths = ['./docs/coretax_telegram.csv']
88
  logger.info("Connected to the MongoDB database!")
89
+ # rag_system = await setup_rag_system()
90
+ engine= RAG_Engine(embedding_model='Snowflake/snowflake-arctic-embed-l-v2.0').from_paths(file_paths, load_csv_as_pandas_dataframe=True, text_col='answer', metadata_cols=['question','images_path'])
91
 
92
+ app.state.rag_system = engine
93
+ app.state.agentMemory = agentMemory
94
+ app.state.memory = memory
95
 
96
  global message_handler, webhook_handler
97
  message_handler = await setup_message_handler()
98
  webhook_handler = WebhookHandler(message_handler)
99
  # collections = app.database.list_collection_names()
100
  # print(f"Collections in {db_name}: {collections}")
101
+ # await load_file_with_markdown_function(rag_system=rag_system, filepaths=indexed_links)
102
  yield
103
  except Exception as e:
104
  logger.error(e)
 
106
  # Initialize Limiter and Prometheus Metrics
107
  limiter = Limiter(key_func=get_remote_address)
108
  app = FastAPI(lifespan=lifespan)
109
+ # Mount the 'images' directory so its files are available under the /images URL path
110
+ app.mount("/images", StaticFiles(directory="images"), name="images")
111
  app.state.limiter = limiter
112
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
113
 
 
121
  webhook_requests = Counter('webhook_requests_total', 'Total webhook requests')
122
  webhook_processing_time = Histogram('webhook_processing_seconds', 'Time spent processing webhook')
123
 
124
+ def get_image_links(image_paths: List[str]) -> List[str]:
125
+ links = []
126
+ for path in image_paths:
127
+ # Remove the surrounding brackets and any extra whitespace
128
+ cleaned = path.strip("[]").strip()
129
+ # Split by comma to get individual image paths
130
+ parts = [part.strip() for part in cleaned.split(",") if part.strip()]
131
+ for part in parts:
132
+ # Assuming the part starts with "images/", extract the filename
133
+ if part.startswith("images/"):
134
+ filename = part.split("/", 1)[1]
135
+ links.append(f"/images/{filename}")
136
+ else:
137
+ links.append(part) # Fallback if the format is unexpected
138
+ return links
139
+
140
+ # @app.get("/image-links")
141
+ # async def image_links_endpoint():
142
+ # image_paths = ['[images/photo_3.jpg, images/photo_16.jpg]']
143
+ # links = get_image_links(image_paths)
144
+ # return {"links": links}
145
  # Start Prometheus metrics server on port 8002
146
  # start_http_server(8002)
147
  # Register webhook routes
 
157
  payload = await request.json()
158
 
159
  rag_system = request.app.state.rag_system
160
+ agentMemory = request.app.state.agentMemory
161
+ memory = request.app.state.memory
162
  # validated_payload = WebhookPayload(**payload) # Validate payload
163
  # logger.info(f"Validated Payload: {validated_payload}")
164
 
 
194
  whatsapp_url=WHATSAPP_API_URL,
195
  gemini_api=GEMINI_API,
196
  rag_system=rag_system,
197
+ agentMemory = agentMemory,
198
+ memory = memory
199
  )
200
  # Return HTTP 200 immediately
201
  return JSONResponse(
app/memory/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+
2
+ from .memory import AgentMemory
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = [
6
+
7
+ "AgentMemory"
8
+ ]
app/memory/implementation/async_memory.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # implementations/async_memory.py
2
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ from app.settings import DatabaseSettings, MemorySettings
6
+ from app.memory.memory import ConversationMemoryInterface
7
+ from app.utils.token_counter import SimpleTokenCounter, TikTokenCounter
8
+ from app.memory.models.base import Base
9
+ from app.memory.models.message import Message
10
+ from app.memory.models.user import User
11
+ from typing import List, Dict, Optional
12
+ from datetime import datetime
13
+ from zoneinfo import ZoneInfo
14
+ from sqlalchemy.future import select
15
+
16
+ class AsyncPostgresConversationMemory(ConversationMemoryInterface):
17
+ def __init__(self, db_settings: DatabaseSettings, memory_settings: MemorySettings):
18
+ self.engine = create_async_engine(
19
+ db_settings.url,
20
+ pool_size=db_settings.pool_size,
21
+ max_overflow=db_settings.max_overflow,
22
+ pool_timeout=db_settings.pool_timeout
23
+ )
24
+
25
+ self.async_session = sessionmaker(
26
+ self.engine, class_=AsyncSession, expire_on_commit=False
27
+ )
28
+ self.token_limit = memory_settings.token_limit
29
+
30
+
31
+ if memory_settings.token_counter == "tiktoken":
32
+ self.token_counter = TikTokenCounter(memory_settings.model_name)
33
+ else:
34
+ self.token_counter = SimpleTokenCounter()
35
+
36
+ async def initialize(self):
37
+ """Initialize the database by creating all tables."""
38
+ async with self.engine.begin() as conn:
39
+ await conn.run_sync(Base.metadata.create_all)
40
+
41
+ # In your async_memory.py
42
+ async def add_message(self, username: str, role: str, message: str, timestamp: Optional[datetime] = None) -> None:
43
+ from app.memory.models.user import User # Import here to avoid circular dependencies
44
+ async with self.async_session() as session:
45
+ # Look up the user by username
46
+ result = await session.execute(select(User).filter_by(username=username))
47
+ user = result.scalars().first()
48
+ if user is None:
49
+ raise ValueError(f"User with username '{username}' not found")
50
+
51
+ if timestamp is None:
52
+ timestamp = datetime.now(ZoneInfo("Asia/Jakarta"))
53
+
54
+ # Create the message using the found user's id
55
+ msg = Message(user_id=user.id, role=role, message=message, timestamp=timestamp)
56
+ session.add(msg)
57
+ await session.commit()
58
+ await self.trim_memory_if_needed(session)
59
+
60
+
61
+
62
+ async def get_all_history(self) -> List[Dict]:
63
+ async with self.async_session() as session:
64
+ result = await session.execute(
65
+ select(Message).order_by(Message.timestamp)
66
+ )
67
+ messages = result.scalars().all()
68
+ return [{"role": msg.role, "content": msg.message} for msg in messages]
69
+
70
+ async def get_history(
71
+ self,
72
+ username: Optional[str] = None,
73
+ token_limit: Optional[int] = None,
74
+ last_n: Optional[int] = None
75
+ ) -> List[Dict]:
76
+ async with self.async_session() as session:
77
+ # Build the base query
78
+ query = select(Message).order_by(Message.timestamp)
79
+ if username is not None:
80
+ # Join with User table and filter by username
81
+ query = query.join(User).filter(User.username == username)
82
+ result = await session.execute(query)
83
+ messages = result.scalars().all()
84
+
85
+ # Accumulate messages in reverse (latest first)
86
+ selected = []
87
+ total_tokens = 0
88
+ for msg in reversed(messages):
89
+ tokens = self.token_counter.count_tokens(msg.message)
90
+ # If token_limit is specified and no message has been added yet,
91
+ # force-add the last message even if it exceeds token_limit.
92
+ if token_limit is not None and len(selected) == 0 and tokens > token_limit:
93
+ selected.append(msg)
94
+ total_tokens = tokens
95
+ continue
96
+ # Otherwise, check if adding this message would exceed the token limit.
97
+ if token_limit is not None and total_tokens + tokens > token_limit:
98
+ break
99
+ selected.append(msg)
100
+ total_tokens += tokens
101
+ # Stop if we've reached the maximum number of messages.
102
+ if last_n is not None and len(selected) >= last_n:
103
+ break
104
+
105
+ # Reverse to return in chronological order
106
+ selected.reverse()
107
+ return [{"role": msg.role, "parts": msg.message} for msg in selected]
108
+
109
+
110
+ async def clear_memory(self) -> None:
111
+ async with self.async_session() as session:
112
+ await session.execute(select(Message).delete())
113
+ await session.commit()
114
+
115
+ async def get_total_tokens(self) -> int:
116
+ async with self.async_session() as session:
117
+ result = await session.execute(select(Message))
118
+ messages = result.scalars().all()
119
+ return sum(self.token_counter.count_tokens(msg.message) for msg in messages)
120
+
121
+ async def trim_memory_if_needed(self, session: AsyncSession) -> None:
122
+ result = await session.execute(select(Message).order_by(Message.timestamp))
123
+ messages = result.scalars().all()
124
+ total_tokens = sum(self.token_counter.count_tokens(msg.message) for msg in messages)
125
+
126
+ while total_tokens > self.token_limit and messages:
127
+ oldest = messages.pop(0)
128
+ total_tokens -= self.token_counter.count_tokens(oldest.message)
129
+ await session.delete(oldest)
130
+
131
+ await session.commit()
app/memory/memory.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Dict, Optional
3
+ from datetime import datetime
4
+ from zoneinfo import ZoneInfo
5
+
6
+
7
+ class ConversationMemoryInterface(ABC):
8
+ @abstractmethod
9
+ def add_message(self, role: str, message: str, timestamp: Optional[datetime] = None) -> None:
10
+ pass
11
+
12
+ @abstractmethod
13
+ def get_history(self) -> List[Dict]:
14
+ pass
15
+
16
+ @abstractmethod
17
+ def clear_memory(self) -> None:
18
+ pass
19
+
20
+ @abstractmethod
21
+ def get_total_tokens(self) -> int:
22
+ pass
23
+
24
+
25
+ from app.settings import DatabaseSettings, MemorySettings, settings
26
+ from app.memory.implementation.async_memory import AsyncPostgresConversationMemory
27
+ from datetime import datetime
28
+ from sqlalchemy.future import select
29
+
30
+ class AgentMemory:
31
+ def __init__(
32
+ self,
33
+ db_url: str = None,
34
+ token_limit: int = 500,
35
+ token_counter: str = "simple", # or "tiktoken"
36
+ model_name: str = None # required if token_counter == "tiktoken"
37
+ ):
38
+ # Use provided URL or default from settings
39
+ if db_url is None:
40
+ db_url = settings.POSTGRES_DB_URL
41
+ self.db_settings = DatabaseSettings(url=db_url)
42
+ self.memory_settings = MemorySettings(
43
+ token_limit=token_limit,
44
+ token_counter=token_counter,
45
+ model_name=model_name
46
+ )
47
+ # Instantiate your async memory
48
+ self.memory = AsyncPostgresConversationMemory(self.db_settings, self.memory_settings)
49
+
50
+ async def initialize(self):
51
+ """Initializes the database tables and returns the memory instance."""
52
+ await self.memory.initialize()
53
+ return self.memory
54
+
55
+ async def add_user(self, username: str, hashed_password: str):
56
+ """
57
+ Adds a new user to the database.
58
+ Returns the created user or existing user if found.
59
+ """
60
+ from app.memory.models.user import User # Import here to avoid circular dependencies
61
+ async with self.memory.async_session() as session:
62
+ result = await session.execute(select(User).filter_by(username=username))
63
+ existing_user = result.scalars().first()
64
+ if existing_user:
65
+ return existing_user
66
+
67
+ new_user = User(
68
+ username=username,
69
+ hashed_password=hashed_password,
70
+ created_at=datetime.now(ZoneInfo("Asia/Jakarta"))
71
+ )
72
+ session.add(new_user)
73
+ await session.commit()
74
+ return new_user
75
+
app/memory/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # models/__init__.py
2
+ from .user import User
3
+ from .message import Message
app/memory/models/base.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from sqlalchemy.orm import declarative_base
2
+ Base = declarative_base()
app/memory/models/message.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models/message.py
2
+ from datetime import datetime
3
+ from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
4
+ from sqlalchemy.orm import relationship
5
+ from zoneinfo import ZoneInfo
6
+ from .base import Base
7
+
8
+ class Message(Base):
9
+ __tablename__ = 'messages'
10
+
11
+ id = Column(Integer, primary_key=True)
12
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
13
+ role = Column(String(50))
14
+ message = Column(Text)
15
+ timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(ZoneInfo("Asia/Jakarta")))
16
+
17
+ # Use a string reference for deferred resolution.
18
+ user = relationship("User", back_populates="messages")
app/memory/models/user.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models/user.py
2
+ from datetime import datetime
3
+ from sqlalchemy import Column, Integer, String, DateTime
4
+ from sqlalchemy.orm import relationship
5
+ from zoneinfo import ZoneInfo
6
+ from .base import Base
7
+
8
+ class User(Base):
9
+ __tablename__ = 'users'
10
+
11
+ id = Column(Integer, primary_key=True)
12
+ username = Column(String(100), unique=True, nullable=False)
13
+ hashed_password = Column(String(255), nullable=False)
14
+ created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(ZoneInfo("Asia/Jakarta")))
15
+
16
+ # Relationship to Message
17
+ messages = relationship("Message", back_populates="user", cascade="all, delete-orphan")
app/services/message.py CHANGED
@@ -5,21 +5,71 @@ from typing import Dict, Any, Optional, List
5
  from datetime import datetime
6
  import logging
7
  import asyncio
 
8
  from openai import AsyncOpenAI
9
- import json
10
  import google.generativeai as genai
11
-
12
  import PIL.Image
 
13
  from typing import List, Dict, Any, Optional
14
 
15
- from app.utils.load_env import ACCESS_TOKEN, WHATSAPP_API_URL, GEMINI_API
16
  from app.utils.system_prompt import system_prompt
17
 
18
  from app.services.search_engine import google_search
19
- from app.search.rag_pipeline import extract_keywords_async
 
 
 
 
 
 
 
20
  # Load environment variables
21
  load_dotenv()
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  # Define function specifications for Gemini
24
  function_declarations = [
25
  {
@@ -43,6 +93,17 @@ function_declarations = [
43
  }
44
  ]
45
 
 
 
 
 
 
 
 
 
 
 
 
46
  genai.configure(api_key=GEMINI_API)
47
  # client = AsyncOpenAI(api_key = OPENAI_API)
48
  # Configure logging
@@ -56,13 +117,77 @@ logger = logging.getLogger(__name__)
56
  if not WHATSAPP_API_URL or not ACCESS_TOKEN:
57
  logger.warning("Environment variables for WHATSAPP_API_URL or ACCESS_TOKEN are not set!")
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  # Helper function to send a reply
60
- async def send_reply(to: str, body: str, whatsapp_token: str, whatsapp_url:str) -> Dict[str, Any]:
61
  headers = {
62
  "Authorization": f"Bearer {whatsapp_token}",
63
  "Content-Type": "application/json"
64
  }
65
- data = {
66
  "messaging_product": "whatsapp",
67
  "to": to,
68
  "type": "text",
@@ -71,15 +196,46 @@ async def send_reply(to: str, body: str, whatsapp_token: str, whatsapp_url:str)
71
  }
72
  }
73
 
74
- async with httpx.AsyncClient() as client:
75
- response = await client.post(whatsapp_url, json=data, headers=headers)
76
 
77
- if response.status_code != 200:
78
- error_detail = response.json()
79
- logger.error(f"Failed to send reply: {error_detail}")
80
- raise Exception(f"Failed to send reply with status code {response.status_code}: {error_detail}")
81
-
82
- return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  # Helper function to generate a reply based on message content
85
  async def generate_reply(sender: str, content: str, timestamp: int) -> str:
@@ -102,8 +258,11 @@ async def process_message_with_llm(
102
  content: str,
103
  history: List[Dict[str, str]],
104
  rag_system: Any,
 
105
  whatsapp_token: str,
106
  whatsapp_url:str,
 
 
107
  image_file_path: Optional[str] = None,
108
  doc_path: Optional[str] = None,
109
  video_file_path: Optional[str] = None,
@@ -111,29 +270,119 @@ async def process_message_with_llm(
111
  """Process message with retry logic."""
112
  try:
113
  logger.info(f"Processing message for sender: {sender_id}")
114
- generated_reply = await generate_response_from_gemini(
115
  sender=sender_id,
116
  content=content,
117
  history=history,
118
  rag_system=rag_system,
119
  image_file_path=image_file_path,
120
  doc_path=doc_path,
121
- video_file_path=video_file_path
 
 
122
  )
123
- logger.info(f"Generated reply: {generated_reply}")
124
 
125
- response = await send_reply(sender_id, generated_reply, whatsapp_token, whatsapp_url)
126
  # return generated_reply
127
  return generated_reply
128
  except Exception as e:
129
  logger.error(f"Error in process_message_with_retry: {str(e)}", exc_info=True)
130
  return "Sorry, I couldn't generate a response at this time."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
 
 
 
 
132
  async def generate_response_from_gemini(
133
  sender: str,
134
  content: str,
135
  history: List[Dict[str, str]],
136
  rag_system: Any = None,
 
 
137
  image_file_path: Optional[str] = None,
138
  doc_path: Optional[str] = None,
139
  video_file_path: Optional[str] = None,
@@ -151,15 +400,18 @@ async def generate_response_from_gemini(
151
 
152
  if content:
153
  if rag_system:
154
- keywords = extract_keywords_async(content)
155
  # keywords = []
156
  # logger.info(f"Extracted Keywords: {keywords}")
157
  # Implement RAG: Retrieve relevant documents
158
- retrieved_docs = await rag_system.adv_query(content, keywords=keywords, top_k=5)
 
 
159
  if retrieved_docs:
160
  logger.info(f"Retrieved {len(retrieved_docs)} documents for context.")
161
  # Format the retrieved documents as a context string
162
- context = "\n\n".join([f"Source:{doc['url']}\nContent: {doc['text']}" for doc in retrieved_docs])
 
163
  # Option 1: Append to history as a system message
164
  history.append({"role": "user", "parts": f"Relevant documents:\n{context}"})
165
 
@@ -192,8 +444,13 @@ async def generate_response_from_gemini(
192
 
193
  # Send the user's message
194
  response = await chat.send_message_async(content)
 
 
 
 
195
  # response = await handle_function_call(response)
196
- return response.text
 
197
 
198
  except Exception as e:
199
  logger.error("Error in generate_response_from_gemini:", exc_info=True)
 
5
  from datetime import datetime
6
  import logging
7
  import asyncio
8
+ import hashlib
9
  from openai import AsyncOpenAI
10
+ import json, requests, mimetypes
11
  import google.generativeai as genai
12
+ import re, json
13
  import PIL.Image
14
+ import requests
15
  from typing import List, Dict, Any, Optional
16
 
17
+ from app.utils.load_env import ACCESS_TOKEN, WHATSAPP_API_URL, GEMINI_API, MEDIA_UPLOAD_URL
18
  from app.utils.system_prompt import system_prompt
19
 
20
  from app.services.search_engine import google_search
21
+ # from app.search.rag_pipeline import extract_keywords_async
22
+
23
+ from vidavox.core import (
24
+
25
+ BaseResultFormatter,
26
+ SearchResult)
27
+
28
+
29
  # Load environment variables
30
  load_dotenv()
31
 
32
+
33
+
34
+ # Get base url from ngrok
35
+ def get_ngrok_url() -> str:
36
+ """Fetches the public URL of the first ngrok tunnel."""
37
+ try:
38
+ response = requests.get("http://localhost:4040/api/tunnels")
39
+ response.raise_for_status() # Raise an error for bad status codes.
40
+ tunnels = response.json().get("tunnels", [])
41
+ if tunnels:
42
+ # Prefer the HTTPS tunnel if available.
43
+ for tunnel in tunnels:
44
+ if tunnel.get("proto") == "https":
45
+ return tunnel.get("public_url")
46
+ # Fallback: return the first tunnel's URL.
47
+ return tunnels[0].get("public_url")
48
+ except Exception as e:
49
+ print("Error fetching ngrok URL:", e)
50
+ # Fallback in case ngrok isn't running.
51
+ return "http://localhost:8005"
52
+
53
+ base_url = get_ngrok_url() # Automatically retrieve your public ngrok URL
54
+ print("Base URL:", base_url)
55
+ # Get image link from image paths
56
+
57
+ def get_image_links(image_paths: List[str], base_url: str) -> List[str]:
58
+ links = []
59
+ for path in image_paths:
60
+ # Remove the surrounding brackets and any extra whitespace
61
+ cleaned = path.strip("[]").strip()
62
+ # Split by comma to get individual image paths
63
+ parts = [part.strip() for part in cleaned.split(",") if part.strip()]
64
+ for part in parts:
65
+ # Assuming the part starts with "images/", extract the filename
66
+ if part.startswith("images/"):
67
+ filename = part.split("/", 1)[1]
68
+ links.append(f"{base_url}/images/{filename}")
69
+ else:
70
+ links.append(f"{base_url}/{part}") # Fallback if the format is unexpected
71
+ return links
72
+
73
  # Define function specifications for Gemini
74
  function_declarations = [
75
  {
 
93
  }
94
  ]
95
 
96
+ class CustomResultFormatter(BaseResultFormatter):
97
+ def format(self, result: SearchResult) -> Dict[str, Any]:
98
+ # Customize the result format as needed
99
+ return {
100
+ "doc_id": result.doc_id,
101
+
102
+ "page_content": result.text,
103
+ "image": result.meta_data['images_path'],
104
+ "relevance": result.score,
105
+ }
106
+
107
  genai.configure(api_key=GEMINI_API)
108
  # client = AsyncOpenAI(api_key = OPENAI_API)
109
  # Configure logging
 
117
  if not WHATSAPP_API_URL or not ACCESS_TOKEN:
118
  logger.warning("Environment variables for WHATSAPP_API_URL or ACCESS_TOKEN are not set!")
119
 
120
+ # Path for the cache file
121
+ CACHE_FILE = 'upload_cache.json'
122
+ # Load the cache if it exists, otherwise initialize an empty dict
123
+ if os.path.exists(CACHE_FILE):
124
+ with open(CACHE_FILE, 'r') as f:
125
+ upload_cache = json.load(f)
126
+ else:
127
+ upload_cache = {}
128
+
129
+ def save_cache():
130
+ with open(CACHE_FILE, 'w') as f:
131
+ json.dump(upload_cache, f)
132
+
133
+ def compute_file_hash(file_path, block_size=65536):
134
+ """Compute SHA256 hash of a file to uniquely identify its content."""
135
+ hasher = hashlib.sha256()
136
+ with open(file_path, 'rb') as f:
137
+ for block in iter(lambda: f.read(block_size), b''):
138
+ hasher.update(block)
139
+ return hasher.hexdigest()
140
+
141
+ # Helper function to upload an image
142
+ async def upload_image(file_path):
143
+ logger.info(f"Uploading image: {file_path}")
144
+
145
+ # Ensure the file exists
146
+ if not os.path.exists(file_path):
147
+ raise Exception(f"File not found: {file_path}")
148
+
149
+ # Compute a hash for the file to check for previous uploads
150
+ file_hash = compute_file_hash(file_path)
151
+ if file_hash in upload_cache:
152
+ logger.info(f"File {file_path} already uploaded. Returning cached media ID.")
153
+ return upload_cache[file_hash]
154
+
155
+ # Get the MIME type of the file
156
+ mime_type, _ = mimetypes.guess_type(file_path)
157
+ if not mime_type:
158
+ raise Exception(f"Could not determine the MIME type for file: {file_path}")
159
+
160
+ headers = {
161
+ 'Authorization': f'Bearer {ACCESS_TOKEN}'
162
+ }
163
+ # Open the file and prepare the payload for upload
164
+ with open(file_path, 'rb') as video_file:
165
+ files = {
166
+ 'file': (os.path.basename(file_path), video_file, mime_type)
167
+ }
168
+ data = {
169
+ 'messaging_product': 'whatsapp'
170
+ }
171
+ response = requests.post(MEDIA_UPLOAD_URL, headers=headers, files=files, data=data)
172
+
173
+ if response.status_code == 200:
174
+ logger.info(f"Upload successful: {response.text}")
175
+ media_id = response.json()['id']
176
+ # Cache the result so future calls can use the same media ID
177
+ upload_cache[file_hash] = media_id
178
+ save_cache()
179
+ return media_id
180
+ else:
181
+ logger.error(f"Upload failed: {response.text}")
182
+ raise Exception(f'Failed to upload media: {response.status_code}, {response.text}')
183
+
184
  # Helper function to send a reply
185
+ async def send_reply(to: str, body: str, whatsapp_token: str, whatsapp_url:str, image:Any) -> Dict[str, Any]:
186
  headers = {
187
  "Authorization": f"Bearer {whatsapp_token}",
188
  "Content-Type": "application/json"
189
  }
190
+ text_data = {
191
  "messaging_product": "whatsapp",
192
  "to": to,
193
  "type": "text",
 
196
  }
197
  }
198
 
199
+ responses = {} # To store the responses
 
200
 
201
+ async with httpx.AsyncClient() as client:
202
+ # response = await client.post(whatsapp_url, json=text_data, headers=headers)
203
+ text_response = await client.post(whatsapp_url, json=text_data, headers=headers)
204
+ if text_response.status_code != 200:
205
+ error_detail = text_response.json()
206
+ logger.error(f"Failed to send text reply: {error_detail}")
207
+ raise Exception(f"Failed to send text reply with status code {text_response.status_code}: {error_detail}")
208
+ responses["text"] = text_response.json()
209
+ # if response.status_code != 200:
210
+ # error_detail = response.json()
211
+ # logger.error(f"Failed to send reply: {error_detail}")
212
+ # raise Exception(f"Failed to send reply with status code {response.status_code}: {error_detail}")
213
+ # Initialize list to hold image responses
214
+ image_responses: List[Dict[str, Any]] = []
215
+ if image:
216
+ # Get the list of full image URLs using your helper function.
217
+ links = get_image_links(image, base_url)
218
+ for link in links:
219
+ image_payload = {
220
+ "messaging_product": "whatsapp",
221
+ "recipient_type": "individual",
222
+ "to": to,
223
+ "type": "image",
224
+ "image": {
225
+ "id": "",
226
+ "link": link,
227
+ "caption": "" # Using the text body as caption; adjust if needed.
228
+ }
229
+ }
230
+ img_response = await client.post(whatsapp_url, json=image_payload, headers=headers)
231
+ if img_response.status_code != 200:
232
+ error_detail = img_response.json()
233
+ logger.error(f"Failed to send image: {error_detail}")
234
+ raise Exception(f"Failed to send image with status code {img_response.status_code}: {error_detail}")
235
+ image_responses.append(img_response.json())
236
+ responses["images"] = image_responses
237
+ return responses
238
+ # return response.json()
239
 
240
  # Helper function to generate a reply based on message content
241
  async def generate_reply(sender: str, content: str, timestamp: int) -> str:
 
258
  content: str,
259
  history: List[Dict[str, str]],
260
  rag_system: Any,
261
+
262
  whatsapp_token: str,
263
  whatsapp_url:str,
264
+ agentMemory: Any = None,
265
+ memory:Any = None,
266
  image_file_path: Optional[str] = None,
267
  doc_path: Optional[str] = None,
268
  video_file_path: Optional[str] = None,
 
270
  """Process message with retry logic."""
271
  try:
272
  logger.info(f"Processing message for sender: {sender_id}")
273
+ generated_reply, image_path = await generate_response_from_gemini(
274
  sender=sender_id,
275
  content=content,
276
  history=history,
277
  rag_system=rag_system,
278
  image_file_path=image_file_path,
279
  doc_path=doc_path,
280
+ video_file_path=video_file_path,
281
+ agentMemory=agentMemory,
282
+ memory = memory
283
  )
284
+ logger.info(f"Generated reply: {generated_reply}, extracted image path: {image_path}")
285
 
286
+ response = await send_reply(sender_id, generated_reply , whatsapp_token, whatsapp_url, image_path)
287
  # return generated_reply
288
  return generated_reply
289
  except Exception as e:
290
  logger.error(f"Error in process_message_with_retry: {str(e)}", exc_info=True)
291
  return "Sorry, I couldn't generate a response at this time."
292
+
293
+ import markdown
294
+ from bs4 import BeautifulSoup
295
+
296
+ def format_response_text(response_text: str) -> str:
297
+ """
298
+ Converts markdown-formatted text to plain text with proper newlines.
299
+ This will ensure bullet points, paragraphs, and other elements are formatted
300
+ for display in WhatsApp.
301
+ """
302
+ # Convert markdown to HTML
303
+ html = markdown.markdown(response_text)
304
+ # Parse HTML and extract text using newline as separator
305
+ soup = BeautifulSoup(html, "html.parser")
306
+ formatted_text = soup.get_text(separator="\n")
307
+ return formatted_text
308
+
309
+ import re
310
+ import json
311
+
312
+ def process_llm_response(llm_output):
313
+ # If it's a string, attempt to extract JSON from markdown code fences.
314
+ if isinstance(llm_output, str):
315
+ pattern = r"```json\s*(\{.*\})\s*```"
316
+ match = re.search(pattern, llm_output, re.DOTALL)
317
+ if match:
318
+ json_str = match.group(1)
319
+ else:
320
+ json_str = llm_output.strip()
321
+ try:
322
+ parsed = json.loads(json_str)
323
+ if isinstance(parsed, dict) and "response" in parsed:
324
+ response_text = parsed.get("response", "")
325
+ # Optionally format the response text using our helper
326
+ # formatted_response = format_response_text(response_text)
327
+ references = parsed.get("references", [])
328
+ if isinstance(references, list):
329
+ image_paths = [ref.get("image") for ref in references
330
+ if ref.get("image") and ref.get("image") != "nan"]
331
+ else:
332
+ image_paths = []
333
+ return response_text, image_paths
334
+ else:
335
+ # Fallback if the JSON doesn't have expected structure.
336
+ return llm_output, []
337
+ except json.JSONDecodeError:
338
+ # Fallback: if JSON parsing fails, assume it's plain text.
339
+ return format_response_text(llm_output), []
340
+
341
+ # If not a string, return something sensible.
342
+ return str(llm_output), []
343
+
344
+
345
+
346
+ # def process_llm_response(llm_output):
347
+ # # If it's a string, attempt to extract JSON from markdown code fences.
348
+ # if isinstance(llm_output, str):
349
+ # # Try to capture JSON content if it's wrapped in ```json ... ```
350
+ # pattern = r"```json\s*(\{.*\})\s*```"
351
+ # match = re.search(pattern, llm_output, re.DOTALL)
352
+ # if match:
353
+ # json_str = match.group(1)
354
+ # else:
355
+ # json_str = llm_output.strip()
356
+ # try:
357
+ # parsed = json.loads(json_str)
358
+ # # Check if parsed output has the expected keys.
359
+ # if isinstance(parsed, dict) and "response" in parsed:
360
+ # response_text = parsed.get("response", "")
361
+ # references = parsed.get("references", [])
362
+ # if isinstance(references, list):
363
+ # image_paths = [ref.get("image") for ref in references
364
+ # if ref.get("image") and ref.get("image") != "nan"]
365
+ # else:
366
+ # image_paths = []
367
+ # return response_text, image_paths
368
+ # else:
369
+ # # Fallback: parsed JSON does not have the expected structure.
370
+ # return llm_output, []
371
+ # except json.JSONDecodeError:
372
+ # # Fallback: if JSON parsing fails, assume it's plain text.
373
+ # return llm_output, []
374
 
375
+ # # If not a string, ensure we return something sensible.
376
+ # return str(llm_output), []
377
+
378
+
379
  async def generate_response_from_gemini(
380
  sender: str,
381
  content: str,
382
  history: List[Dict[str, str]],
383
  rag_system: Any = None,
384
+ agentMemory: Any = None,
385
+ memory:Any = None,
386
  image_file_path: Optional[str] = None,
387
  doc_path: Optional[str] = None,
388
  video_file_path: Optional[str] = None,
 
400
 
401
  if content:
402
  if rag_system:
403
+ # keywords = extract_keywords_async(content)
404
  # keywords = []
405
  # logger.info(f"Extracted Keywords: {keywords}")
406
  # Implement RAG: Retrieve relevant documents
407
+ retrieved_docs = rag_system.retrieve(query_text = content, result_formatter=CustomResultFormatter())
408
+
409
+ print(f"retrieved docs: {retrieved_docs}")
410
  if retrieved_docs:
411
  logger.info(f"Retrieved {len(retrieved_docs)} documents for context.")
412
  # Format the retrieved documents as a context string
413
+ context = "\n\n".join([f"Source:{doc['doc_id']}\nContent: {doc['page_content']}\nImage: {doc['image']}" for doc in retrieved_docs])
414
+ # img_paths = doc['images_path'] for doc in retrieved_docs
415
  # Option 1: Append to history as a system message
416
  history.append({"role": "user", "parts": f"Relevant documents:\n{context}"})
417
 
 
444
 
445
  # Send the user's message
446
  response = await chat.send_message_async(content)
447
+
448
+ print(f"text: {response.text}")
449
+
450
+ response_text, image_paths = process_llm_response(response.text)
451
  # response = await handle_function_call(response)
452
+ # return response.text
453
+ return response_text, image_paths
454
 
455
  except Exception as e:
456
  logger.error("Error in generate_response_from_gemini:", exc_info=True)
app/settings.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+ @dataclass
11
+ class DatabaseSettings:
12
+ url: str
13
+ pool_size: int = 5
14
+ max_overflow: int = 10
15
+ pool_timeout: int = 30
16
+
17
+ @dataclass
18
+ class MemorySettings:
19
+ token_limit: int = 4096
20
+ token_counter: str = "simple" # "simple" or "tiktoken"
21
+ model_name: Optional[str] = None #
22
+
23
+
24
+ class Settings:
25
+ POSTGRES_DB_URL: str = os.getenv("POSTGRES_DB_URL")
26
+ print(POSTGRES_DB_URL)
27
+ SQLITE_DB_URL: str = os.getenv("SQLITE_DB_URL")
28
+ # Add other settings as needed
29
+
30
+ settings = Settings()
app/utils/load_env.py CHANGED
@@ -18,6 +18,7 @@ OPENAI_API = os.getenv("OPENAI_API")
18
  GEMINI_API = os.getenv("GEMINI_API")
19
  CX_CODE = os.getenv("CX_CODE")
20
  CUSTOM_SEARCH_API_KEY = os.getenv("CUSTOM_SEARCH_API_KEY")
 
21
 
22
  # Debugging: Print the retrieved ACCESS_TOKEN (for development only)
23
  # if ENV == "development":
 
18
  GEMINI_API = os.getenv("GEMINI_API")
19
  CX_CODE = os.getenv("CX_CODE")
20
  CUSTOM_SEARCH_API_KEY = os.getenv("CUSTOM_SEARCH_API_KEY")
21
+ MEDIA_UPLOAD_URL = os.getenv("WHATSAPP_UPLOAD_MEDIA")
22
 
23
  # Debugging: Print the retrieved ACCESS_TOKEN (for development only)
24
  # if ENV == "development":
app/utils/system_prompt.py CHANGED
@@ -1,27 +1,62 @@
1
  system_prompt = """
2
  Role and Purpose:
3
- You are a virtual assistant focused exclusively on Surabaya, Indonesia. Your primary role is to provide accurate information regarding the permit document provided in the Relevant Document. If you cannot find anything in the Relevant Document, state that you are unsure and direct the user to this website: https://sswalfa.surabaya.go.id/ without bracket or parentheses. You respond only in Bahasa Indonesia. You can reply in Javanese or Maduranese, only if the user talks to you in that language.
4
 
5
  Tone and Style:
6
- Maintain a polite, neutral, and factual tone. Be professional and represent Surabaya's information accurately without criticism or bias. Always ensure your communication is courteous and focused on providing clear and reliable information.
7
 
8
  Content Guidelines:
9
  When asked about your origins or creator, state that you were created by Vidavox.
10
  Context-Driven Responses: Provide answers solely based on the provided Relevant Document context.
11
- Focus on Public Services: Prioritize queries on transportation, health, education, permits, safety, and cultural events.
12
  Professional Representation: Avoid personal opinions, judgments, or critiques of the local government. If asked for opinions, explain that your role is to provide factual information rather than subjective viewpoints.
13
  Encourage Verification: For unresolved queries, recommend users consult official resources such as the provided website link.
14
- Always Include Sources: When your response is based on information provided from external sources or Relevant Document, include the source link explicitly without brackets or parentheses at the end of the response. For example: "Informasi ini berasal dari www.indosource.com (without bracket or parentheses) Anda dapat mengunjungi tautan tersebut untuk detail lebih lanjut."
 
 
 
 
 
 
 
 
 
15
 
16
- Example Interactions:
 
 
 
 
 
 
 
17
 
18
- If a user asks, “How is the Mass Rapid Transit project progressing?” you might say: “As of the latest information available, the Surabaya Mass Rapid Transit project is currently in [X] phase, with construction ongoing in [specific districts]. The city's transportation department has announced that the project aims to be operational by [target year]. You can check the official city transportation website for updates.”
19
- If a user says, “I heard there will be a community festival next month, can you tell me more?” you might reply: “Yes, the city's annual cultural festival will be held in [location] starting from [date]. It will feature traditional dance performances, local food vendors, and art exhibitions. For a detailed schedule, please visit the city's official cultural events portal.”
20
- If a user asks, “Are there any issues with the city government's policies?” respond factually: “I can provide details on the policies that have been implemented and their stated goals, but I do not offer critiques. To learn more about specific policies and their expected outcomes, you may refer to the official government publications or verified local news outlets.”
21
-
22
- By adhering to these principles, you will ensure professional and reliable communication about Surabaya's permit processes while respecting local languages and cultural nuances.
23
  """
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  agentic_prompt = """ You are a helpful assistant and have capabilities to search the web.
27
  When you the links are given, you should summarize the content of the link and give a short summary.
 
1
  system_prompt = """
2
  Role and Purpose:
3
+ You are a virtual assistant focused exclusively on coretax, taxation systme in Indonesia. Your primary role is to provide accurate information regarding coretax in the Relevant Document. If you cannot find anything in the Relevant Document, state that you are unsure and direct the user to this website: https://www.pajak.go.id/reformdjp/Coretax/ without bracket or parentheses. You respond only in Bahasa Indonesia.
4
 
5
  Tone and Style:
6
+ Maintain a polite, neutral, and factual tone. Be professional and represent Direktorat Jenderal Pajak accurately without criticism or bias. Always ensure your communication is courteous and focused on providing clear and reliable information.
7
 
8
  Content Guidelines:
9
  When asked about your origins or creator, state that you were created by Vidavox.
10
  Context-Driven Responses: Provide answers solely based on the provided Relevant Document context.
 
11
  Professional Representation: Avoid personal opinions, judgments, or critiques of the local government. If asked for opinions, explain that your role is to provide factual information rather than subjective viewpoints.
12
  Encourage Verification: For unresolved queries, recommend users consult official resources such as the provided website link.
13
+ You don't need to say you refer to the relevant document in providing answer.
14
+
15
+ Response Guidelines:
16
+ You'll receive context in the following example: [{'doc_id':doc.csv, 'page_content':'loremipsum..','image':'[images/photo.jgp']}].
17
+ When you use the context, you should provide response in the following rules.
18
+ For each response, return a JSON output with two keys:
19
+ 1. "response": Your generated answer to the user, ensuring it does not reference specific metadata like image paths.
20
+ 2. "references": A list of metadata objects containing the document ID and the associated image path.
21
+ Ensure that the response does not explicitly mention or display image paths.
22
+ Ensure the response returned in a well formatted format.
23
 
24
+ # Example LLM response
25
+ {
26
+ "response": ""# Introduction\nThis is an example.\n\n- Bullet point 1\n- Bullet point 2\n\n## Sub-Topic\nAdditional details...",
27
+ "references": [
28
+ {"doc_id": "123", "image": "images/paris.jpg"},
29
+ {"doc_id": "456", "image": "images/eiffel.jpg"}
30
+ ]
31
+ }
32
 
33
+ By adhering to these principles, you will ensure professional and reliable communication about coretax system under Direktorat Jenderal Pajak.
 
 
 
 
34
  """
35
 
36
+ # system_prompt = """
37
+ # Role and Purpose:
38
+ # You are a virtual assistant focused exclusively on Surabaya, Indonesia. Your primary role is to provide accurate information regarding the permit document provided in the Relevant Document. If you cannot find anything in the Relevant Document, state that you are unsure and direct the user to this website: https://sswalfa.surabaya.go.id/ without bracket or parentheses. You respond only in Bahasa Indonesia. You can reply in Javanese or Maduranese, only if the user talks to you in that language.
39
+
40
+ # Tone and Style:
41
+ # Maintain a polite, neutral, and factual tone. Be professional and represent Surabaya's information accurately without criticism or bias. Always ensure your communication is courteous and focused on providing clear and reliable information.
42
+
43
+ # Content Guidelines:
44
+ # When asked about your origins or creator, state that you were created by Vidavox.
45
+ # Context-Driven Responses: Provide answers solely based on the provided Relevant Document context.
46
+ # Focus on Public Services: Prioritize queries on transportation, health, education, permits, safety, and cultural events.
47
+ # Professional Representation: Avoid personal opinions, judgments, or critiques of the local government. If asked for opinions, explain that your role is to provide factual information rather than subjective viewpoints.
48
+ # Encourage Verification: For unresolved queries, recommend users consult official resources such as the provided website link.
49
+ # Always Include Sources: When your response is based on information provided from external sources or Relevant Document, include the source link explicitly without brackets or parentheses at the end of the response. For example: "Informasi ini berasal dari www.indosource.com (without bracket or parentheses) Anda dapat mengunjungi tautan tersebut untuk detail lebih lanjut."
50
+
51
+ # Example Interactions:
52
+
53
+ # If a user asks, “How is the Mass Rapid Transit project progressing?” you might say: “As of the latest information available, the Surabaya Mass Rapid Transit project is currently in [X] phase, with construction ongoing in [specific districts]. The city's transportation department has announced that the project aims to be operational by [target year]. You can check the official city transportation website for updates.”
54
+ # If a user says, “I heard there will be a community festival next month, can you tell me more?” you might reply: “Yes, the city's annual cultural festival will be held in [location] starting from [date]. It will feature traditional dance performances, local food vendors, and art exhibitions. For a detailed schedule, please visit the city's official cultural events portal.”
55
+ # If a user asks, “Are there any issues with the city government's policies?” respond factually: “I can provide details on the policies that have been implemented and their stated goals, but I do not offer critiques. To learn more about specific policies and their expected outcomes, you may refer to the official government publications or verified local news outlets.”
56
+
57
+ # By adhering to these principles, you will ensure professional and reliable communication about Surabaya's permit processes while respecting local languages and cultural nuances.
58
+ # """
59
+
60
 
61
  agentic_prompt = """ You are a helpful assistant and have capabilities to search the web.
62
  When you the links are given, you should summarize the content of the link and give a short summary.
app/utils/token_counter.py CHANGED
@@ -26,4 +26,16 @@ class TokenCounter:
26
  del self.doc_tokens[doc_id]
27
 
28
  def get_total_tokens(self):
29
- return self.total_tokens
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  del self.doc_tokens[doc_id]
27
 
28
  def get_total_tokens(self):
29
+ return self.total_tokens
30
+
31
+ class SimpleTokenCounter:
32
+ def count_tokens(self, text: str) -> int:
33
+ return len(text.split())
34
+
35
+ class TikTokenCounter:
36
+ def __init__(self, model_name: str = "gpt-4"):
37
+ import tiktoken
38
+ self.encoding = tiktoken.encoding_for_model(model_name)
39
+
40
+ def count_tokens(self, text: str) -> int:
41
+ return len(self.encoding.encode(text))
docs/Coretax_FAQ.xlsx ADDED
Binary file (106 kB). View file
 
docs/coretax_telegram.csv ADDED
The diff for this file is too large to render. See raw diff
 
images/photo_10.jpg ADDED
images/photo_107.jpg ADDED
images/photo_108.jpg ADDED
images/photo_11.jpg ADDED
images/photo_112.jpg ADDED
images/photo_12.1.jpg ADDED
images/photo_12.2.jpg ADDED
images/photo_13.jpg ADDED
images/photo_14.jpg ADDED
images/photo_15.1.jpg ADDED
images/photo_15.2.jpg ADDED
images/photo_16.jpg ADDED
images/photo_19.jpg ADDED
images/photo_20.jpg ADDED
images/photo_21.jpg ADDED
images/photo_25.jpg ADDED
images/photo_26.jpg ADDED
images/photo_27.jpg ADDED
images/photo_28.jpg ADDED
images/photo_29.jpg ADDED
images/photo_3.jpg ADDED
images/photo_31.jpg ADDED
images/photo_32.jpg ADDED
images/photo_33.jpg ADDED
images/photo_34.jpg ADDED
images/photo_35.jpg ADDED
images/photo_36.jpg ADDED
images/photo_38.jpg ADDED
images/photo_39.jpg ADDED
images/photo_4.jpg ADDED
images/photo_40.jpg ADDED
images/photo_41.jpg ADDED
images/photo_42.jpg ADDED