Spaces:
Running
Running
V2.1
Browse files- api/gemini_service.py +43 -23
- api/migrations/0005_user_initial_balance.py +19 -0
- api/models.py +1 -0
- api/serializers.py +1 -1
- api/views.py +12 -4
api/gemini_service.py
CHANGED
|
@@ -28,13 +28,15 @@ class GeminiService:
|
|
| 28 |
# Use free tier model with generous quotas
|
| 29 |
self.model = "gemini-flash-latest" # Free tier: 15 RPM, 1M tokens/day
|
| 30 |
|
| 31 |
-
def process_voice_command(self, audio_bytes, mime_type="audio/mp3"):
|
| 32 |
"""
|
| 33 |
Process audio bytes to extract transaction details.
|
| 34 |
Returns a dictionary with transcription and structured data.
|
| 35 |
"""
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
You are an AI assistant for a financial app called Akompta.
|
| 39 |
Your task is to listen to the user's voice command and extract transaction details.
|
| 40 |
|
|
@@ -42,30 +44,37 @@ class GeminiService:
|
|
| 42 |
- "J'ai vendu la tomate pour 500FCFA le Kilo" (Income)
|
| 43 |
- "J'ai payé un ordinateur à 300000FCFA" (Expense)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
Please perform the following:
|
| 46 |
1. Transcribe the audio exactly as spoken (in French).
|
| 47 |
2. Analyze the intent and extract structured data.
|
| 48 |
|
| 49 |
Return ONLY a JSON object with the following structure:
|
| 50 |
-
{
|
| 51 |
"transcription": "The exact transcription",
|
| 52 |
"intent": "create_transaction",
|
| 53 |
-
"data": {
|
| 54 |
"type": "income" or "expense",
|
| 55 |
"amount": number (e.g. 500),
|
| 56 |
"currency": "FCFA" or other,
|
| 57 |
"category": "Category name (e.g. Vente, Alimentation, Transport, Technologie)",
|
| 58 |
-
"name": "Description of the item or service",
|
| 59 |
"date": "YYYY-MM-DD" or null if not specified (assume today if null)
|
| 60 |
-
}
|
| 61 |
-
}
|
|
|
|
|
|
|
| 62 |
|
| 63 |
If the audio is not clear or not related to a transaction, return:
|
| 64 |
-
{
|
| 65 |
"transcription": "...",
|
| 66 |
"intent": "unknown",
|
| 67 |
"error": "Reason"
|
| 68 |
-
}
|
| 69 |
"""
|
| 70 |
|
| 71 |
try:
|
|
@@ -96,11 +105,14 @@ class GeminiService:
|
|
| 96 |
"error": str(e)
|
| 97 |
}
|
| 98 |
|
| 99 |
-
def process_text_command(self, text):
|
| 100 |
"""
|
| 101 |
Process text input to extract transaction details.
|
| 102 |
"""
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
| 104 |
You are an AI assistant for a financial app called Akompta.
|
| 105 |
Your task is to analyze the user's text command and extract transaction details.
|
| 106 |
|
|
@@ -109,45 +121,53 @@ class GeminiService:
|
|
| 109 |
- "J'ai payé un ordinateur à 300000FCFA" (Expense)
|
| 110 |
- "Ajoute un produit Tomate à 200FCFA le bol, j'en ai 30 en stock" (Create Product)
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
Please perform the following:
|
| 113 |
1. Analyze the intent and extract structured data.
|
| 114 |
|
| 115 |
Return ONLY a JSON object with the following structure:
|
| 116 |
|
| 117 |
For transactions:
|
| 118 |
-
{
|
| 119 |
"transcription": "The input text",
|
| 120 |
"intent": "create_transaction",
|
| 121 |
-
"data": {
|
| 122 |
"type": "income" or "expense",
|
| 123 |
"amount": number,
|
| 124 |
"currency": "FCFA",
|
| 125 |
"category": "Category name",
|
| 126 |
-
"name": "Description",
|
| 127 |
"date": "YYYY-MM-DD"
|
| 128 |
-
}
|
| 129 |
-
}
|
|
|
|
|
|
|
| 130 |
|
| 131 |
For products/inventory:
|
| 132 |
-
{
|
| 133 |
"transcription": "The input text",
|
| 134 |
"intent": "create_product",
|
| 135 |
-
"data": {
|
| 136 |
"name": "Product name",
|
| 137 |
"price": number,
|
| 138 |
"unit": "Unit (e.g. bol, kg, unit)",
|
| 139 |
"description": "Short description",
|
| 140 |
"category": "vente" or "depense" or "stock",
|
| 141 |
"stock_status": "ok" or "low" or "rupture"
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
|
| 145 |
If the text is not clear, return:
|
| 146 |
-
{
|
| 147 |
"transcription": "...",
|
| 148 |
"intent": "unknown",
|
| 149 |
"error": "Reason"
|
| 150 |
-
}
|
| 151 |
"""
|
| 152 |
|
| 153 |
try:
|
|
|
|
| 28 |
# Use free tier model with generous quotas
|
| 29 |
self.model = "gemini-flash-latest" # Free tier: 15 RPM, 1M tokens/day
|
| 30 |
|
| 31 |
+
def process_voice_command(self, audio_bytes, mime_type="audio/mp3", context_products=None):
|
| 32 |
"""
|
| 33 |
Process audio bytes to extract transaction details.
|
| 34 |
Returns a dictionary with transcription and structured data.
|
| 35 |
"""
|
| 36 |
+
if context_products is None:
|
| 37 |
+
context_products = []
|
| 38 |
+
|
| 39 |
+
prompt = f"""
|
| 40 |
You are an AI assistant for a financial app called Akompta.
|
| 41 |
Your task is to listen to the user's voice command and extract transaction details.
|
| 42 |
|
|
|
|
| 44 |
- "J'ai vendu la tomate pour 500FCFA le Kilo" (Income)
|
| 45 |
- "J'ai payé un ordinateur à 300000FCFA" (Expense)
|
| 46 |
|
| 47 |
+
Here is the list of products currently in the user's inventory:
|
| 48 |
+
{json.dumps(context_products)}
|
| 49 |
+
|
| 50 |
+
If the user mentions a product from this list but doesn't specify the price, use the price from the list to calculate the total amount (amount = quantity * price).
|
| 51 |
+
|
| 52 |
Please perform the following:
|
| 53 |
1. Transcribe the audio exactly as spoken (in French).
|
| 54 |
2. Analyze the intent and extract structured data.
|
| 55 |
|
| 56 |
Return ONLY a JSON object with the following structure:
|
| 57 |
+
{{
|
| 58 |
"transcription": "The exact transcription",
|
| 59 |
"intent": "create_transaction",
|
| 60 |
+
"data": {{
|
| 61 |
"type": "income" or "expense",
|
| 62 |
"amount": number (e.g. 500),
|
| 63 |
"currency": "FCFA" or other,
|
| 64 |
"category": "Category name (e.g. Vente, Alimentation, Transport, Technologie)",
|
| 65 |
+
"name": "Description of the item or service (e.g. 'Vente de 3 Mangues')",
|
| 66 |
"date": "YYYY-MM-DD" or null if not specified (assume today if null)
|
| 67 |
+
}}
|
| 68 |
+
}}
|
| 69 |
+
|
| 70 |
+
Important: If a product price is found in the inventory list, ALWAYS calculate: amount = quantity * unit_price.
|
| 71 |
|
| 72 |
If the audio is not clear or not related to a transaction, return:
|
| 73 |
+
{{
|
| 74 |
"transcription": "...",
|
| 75 |
"intent": "unknown",
|
| 76 |
"error": "Reason"
|
| 77 |
+
}}
|
| 78 |
"""
|
| 79 |
|
| 80 |
try:
|
|
|
|
| 105 |
"error": str(e)
|
| 106 |
}
|
| 107 |
|
| 108 |
+
def process_text_command(self, text, context_products=None):
|
| 109 |
"""
|
| 110 |
Process text input to extract transaction details.
|
| 111 |
"""
|
| 112 |
+
if context_products is None:
|
| 113 |
+
context_products = []
|
| 114 |
+
|
| 115 |
+
prompt = f"""
|
| 116 |
You are an AI assistant for a financial app called Akompta.
|
| 117 |
Your task is to analyze the user's text command and extract transaction details.
|
| 118 |
|
|
|
|
| 121 |
- "J'ai payé un ordinateur à 300000FCFA" (Expense)
|
| 122 |
- "Ajoute un produit Tomate à 200FCFA le bol, j'en ai 30 en stock" (Create Product)
|
| 123 |
|
| 124 |
+
Here is the list of products currently in the user's inventory:
|
| 125 |
+
{json.dumps(context_products)}
|
| 126 |
+
|
| 127 |
+
If the user mentions a product from this list but doesn't specify the price, use the price from the list to calculate the total amount (amount = quantity * price).
|
| 128 |
+
Example: If "Mangue" is in the list at 100 FCFA and the user says "vendu 3 mangues", the amount should be 300.
|
| 129 |
+
|
| 130 |
Please perform the following:
|
| 131 |
1. Analyze the intent and extract structured data.
|
| 132 |
|
| 133 |
Return ONLY a JSON object with the following structure:
|
| 134 |
|
| 135 |
For transactions:
|
| 136 |
+
{{
|
| 137 |
"transcription": "The input text",
|
| 138 |
"intent": "create_transaction",
|
| 139 |
+
"data": {{
|
| 140 |
"type": "income" or "expense",
|
| 141 |
"amount": number,
|
| 142 |
"currency": "FCFA",
|
| 143 |
"category": "Category name",
|
| 144 |
+
"name": "Description (e.g. 'Vente de 3 Mangues')",
|
| 145 |
"date": "YYYY-MM-DD"
|
| 146 |
+
}}
|
| 147 |
+
}}
|
| 148 |
+
|
| 149 |
+
Important: If a product price is found in the inventory list, ALWAYS calculate: amount = quantity * unit_price.
|
| 150 |
|
| 151 |
For products/inventory:
|
| 152 |
+
{{
|
| 153 |
"transcription": "The input text",
|
| 154 |
"intent": "create_product",
|
| 155 |
+
"data": {{
|
| 156 |
"name": "Product name",
|
| 157 |
"price": number,
|
| 158 |
"unit": "Unit (e.g. bol, kg, unit)",
|
| 159 |
"description": "Short description",
|
| 160 |
"category": "vente" or "depense" or "stock",
|
| 161 |
"stock_status": "ok" or "low" or "rupture"
|
| 162 |
+
}}
|
| 163 |
+
}}
|
| 164 |
|
| 165 |
If the text is not clear, return:
|
| 166 |
+
{{
|
| 167 |
"transcription": "...",
|
| 168 |
"intent": "unknown",
|
| 169 |
"error": "Reason"
|
| 170 |
+
}}
|
| 171 |
"""
|
| 172 |
|
| 173 |
try:
|
api/migrations/0005_user_initial_balance.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.8 on 2026-01-22 21:34
|
| 2 |
+
|
| 3 |
+
from decimal import Decimal
|
| 4 |
+
from django.db import migrations, models
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
dependencies = [
|
| 10 |
+
('api', '0004_aiinsight'),
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.AddField(
|
| 15 |
+
model_name='user',
|
| 16 |
+
name='initial_balance',
|
| 17 |
+
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=15),
|
| 18 |
+
),
|
| 19 |
+
]
|
api/models.py
CHANGED
|
@@ -53,6 +53,7 @@ class User(AbstractUser):
|
|
| 53 |
default='personal'
|
| 54 |
)
|
| 55 |
is_premium = models.BooleanField(default=False)
|
|
|
|
| 56 |
|
| 57 |
# Champs Business
|
| 58 |
business_name = models.CharField(max_length=255, blank=True)
|
|
|
|
| 53 |
default='personal'
|
| 54 |
)
|
| 55 |
is_premium = models.BooleanField(default=False)
|
| 56 |
+
initial_balance = models.DecimalField(max_digits=15, decimal_places=2, default=Decimal('0.00'))
|
| 57 |
|
| 58 |
# Champs Business
|
| 59 |
business_name = models.CharField(max_length=255, blank=True)
|
api/serializers.py
CHANGED
|
@@ -14,7 +14,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|
| 14 |
model = User
|
| 15 |
fields = [
|
| 16 |
'id', 'email', 'first_name', 'last_name', 'phone_number',
|
| 17 |
-
'avatar', 'account_type', 'is_premium',
|
| 18 |
'business_name', 'sector', 'location', 'ifu', 'business_logo',
|
| 19 |
'currency', 'language', 'dark_mode',
|
| 20 |
'created_at', 'updated_at'
|
|
|
|
| 14 |
model = User
|
| 15 |
fields = [
|
| 16 |
'id', 'email', 'first_name', 'last_name', 'phone_number',
|
| 17 |
+
'avatar', 'account_type', 'is_premium', 'initial_balance',
|
| 18 |
'business_name', 'sector', 'location', 'ifu', 'business_logo',
|
| 19 |
'currency', 'language', 'dark_mode',
|
| 20 |
'created_at', 'updated_at'
|
api/views.py
CHANGED
|
@@ -285,7 +285,7 @@ class TransactionViewSet(viewsets.ModelViewSet):
|
|
| 285 |
user=user, type='expense'
|
| 286 |
).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
|
| 287 |
|
| 288 |
-
balance = total_income - total_expenses
|
| 289 |
|
| 290 |
# Variations en %
|
| 291 |
def calc_variation(current, previous):
|
|
@@ -449,7 +449,7 @@ class AnalyticsView(APIView):
|
|
| 449 |
transactions = Transaction.objects.filter(user=user).order_by('date')
|
| 450 |
|
| 451 |
history = []
|
| 452 |
-
running_balance =
|
| 453 |
|
| 454 |
# Grouper par date pour éviter d'avoir trop de points si plusieurs transactions le même jour
|
| 455 |
daily_balances = {}
|
|
@@ -598,12 +598,20 @@ class VoiceCommandView(APIView):
|
|
| 598 |
try:
|
| 599 |
service = GeminiService()
|
| 600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
if audio_file:
|
| 602 |
audio_bytes = audio_file.read()
|
| 603 |
mime_type = audio_file.content_type or 'audio/mp3'
|
| 604 |
-
result = service.process_voice_command(audio_bytes, mime_type)
|
| 605 |
else:
|
| 606 |
-
result = service.process_text_command(text_command)
|
| 607 |
|
| 608 |
print(f"VoiceCommandView - Result Intent: {result.get('intent')}")
|
| 609 |
|
|
|
|
| 285 |
user=user, type='expense'
|
| 286 |
).aggregate(total=Sum('amount'))['total'] or Decimal('0.00')
|
| 287 |
|
| 288 |
+
balance = user.initial_balance + total_income - total_expenses
|
| 289 |
|
| 290 |
# Variations en %
|
| 291 |
def calc_variation(current, previous):
|
|
|
|
| 449 |
transactions = Transaction.objects.filter(user=user).order_by('date')
|
| 450 |
|
| 451 |
history = []
|
| 452 |
+
running_balance = user.initial_balance
|
| 453 |
|
| 454 |
# Grouper par date pour éviter d'avoir trop de points si plusieurs transactions le même jour
|
| 455 |
daily_balances = {}
|
|
|
|
| 598 |
try:
|
| 599 |
service = GeminiService()
|
| 600 |
|
| 601 |
+
# Fetch user products for context
|
| 602 |
+
user_products = Product.objects.filter(user=request.user)
|
| 603 |
+
products_list = [
|
| 604 |
+
{"name": p.name, "price": float(p.price), "unit": p.unit}
|
| 605 |
+
for p in user_products
|
| 606 |
+
]
|
| 607 |
+
print(f"VoiceCommandView - Context Products: {products_list}")
|
| 608 |
+
|
| 609 |
if audio_file:
|
| 610 |
audio_bytes = audio_file.read()
|
| 611 |
mime_type = audio_file.content_type or 'audio/mp3'
|
| 612 |
+
result = service.process_voice_command(audio_bytes, mime_type, context_products=products_list)
|
| 613 |
else:
|
| 614 |
+
result = service.process_text_command(text_command, context_products=products_list)
|
| 615 |
|
| 616 |
print(f"VoiceCommandView - Result Intent: {result.get('intent')}")
|
| 617 |
|