Skip to content

Commit

Permalink
Add the create facts action on external integration (#1967)
Browse files Browse the repository at this point in the history
## deploy plan
- [ ] deploy docs
- [ ] clean the create_memory integration
- [ ] deploy backend
  • Loading branch information
beastoin authored Mar 10, 2025
2 parents a00f444 + 9bfdc96 commit 69a238a
Show file tree
Hide file tree
Showing 13 changed files with 444 additions and 160 deletions.
4 changes: 2 additions & 2 deletions app/lib/pages/apps/widgets/action_fields_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ class ActionFieldsWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
actionType.id == 'create_memory'
? 'Extend user memories by making a POST request to the OMI System.'
actionType.id == 'create_conversation'
? 'Extend user conversations by making a POST request to the OMI System.'
: 'Enable this action for your app.',
style: TextStyle(
color: Colors.grey.shade400,
Expand Down
3 changes: 2 additions & 1 deletion backend/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class AuthStep(BaseModel):


class ActionType(str, Enum):
CREATE_MEMORY = "create_memory"
CREATE_MEMORY = "create_conversation"
CREATE_FACTS = "create_facts"


class Action(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions backend/models/facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class FactDB(Fact):
edited: bool = False
deleted: bool = False
scoring: Optional[str] = None
app_id: Optional[str] = None

@staticmethod
def calculate_score(fact: 'FactDB') -> 'FactDB':
Expand Down
19 changes: 18 additions & 1 deletion backend/models/integrations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum

from models.facts import FactCategory


class MemoryTimestampRange(BaseModel):
Expand All @@ -13,5 +17,18 @@ class ScreenPipeCreateMemory(BaseModel):
timestamp_range: MemoryTimestampRange


class ExternalIntegrationFactSource(str, Enum):
email = "email"
post = "social_post"
other = "other"


class ExternalIntegrationCreateFact(BaseModel):
text: str = Field(description="The original text from which the fact was extracted")
text_source: ExternalIntegrationFactSource = Field(description="The source of the text", default=ExternalIntegrationFactSource.other)
text_source_spec: Optional[str] = Field(description="Additional specification about the source", default=None)
app_id: Optional[str] = None


class EmptyResponse(BaseModel):
pass
2 changes: 0 additions & 2 deletions backend/models/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,6 @@ def get_transcript(self, include_timestamps: bool) -> str:

class ExternalIntegrationMemorySource(str, Enum):
audio = 'audio_transcript'
email = 'email'
post = 'post'
message = 'message'
other = 'other_text'

Expand Down
3 changes: 2 additions & 1 deletion backend/routers/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,8 @@ def get_plugin_capabilities():
{'title': 'Memory Creation', 'id': 'memory_creation'},
{'title': 'Transcript Processed', 'id': 'transcript_processed'},
], 'actions': [
{'title': 'Create memories', 'id': 'create_memory', 'doc_url': 'https://docs.omi.me/docs/developer/apps/IntegrationActions'}
{'title': 'Create conversations', 'id': 'create_conversation', 'doc_url': 'https://docs.omi.me/docs/developer/apps/IntegrationActions'},
{'title': 'Create facts', 'id': 'create_facts', 'doc_url': 'https://docs.omi.me/docs/developer/apps/IntegrationActions'}
]},
{'title': 'Notification', 'id': 'proactive_notification', 'scopes': [
{'title': 'User Name', 'id': 'user_name'},
Expand Down
57 changes: 50 additions & 7 deletions backend/routers/integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from datetime import datetime, timedelta, timezone
from typing import Annotated, Optional
from typing import Annotated, Optional, List

from fastapi import APIRouter, Header, HTTPException, Depends
from fastapi import Request
Expand All @@ -14,13 +14,14 @@
import models.memory as memory_models
from routers.memories import process_memory, trigger_external_integrations
from utils.memories.location import get_google_maps_location
from utils.memories.facts import process_external_integration_fact

router = APIRouter()


@router.post('/v2/integrations/{app_id}/user/memories', response_model=integration_models.EmptyResponse,
tags=['integration', 'memories'])
async def create_memory_via_integration(
@router.post('/v2/integrations/{app_id}/user/conversations', response_model=integration_models.EmptyResponse,
tags=['integration', 'conversations'])
async def create_conversation_via_integration(
request: Request,
app_id: str,
create_memory: memory_models.ExternalIntegrationCreateMemory,
Expand All @@ -45,9 +46,9 @@ async def create_memory_via_integration(
if app_id not in enabled_plugins:
raise HTTPException(status_code=403, detail="App is not enabled for this user")

# Check if the app has the capability external_integration > action > create_memory
if not apps_utils.app_has_action(app, 'create_memory'):
raise HTTPException(status_code=403, detail="App does not have the capability to create memories")
# Check if the app has the capability external_integration > action > create_conversation
if not apps_utils.app_has_action(app, 'create_conversation'):
raise HTTPException(status_code=403, detail="App does not have the capability to create conversations")

# Time
started_at = create_memory.started_at if create_memory.started_at is not None else datetime.now(timezone.utc)
Expand Down Expand Up @@ -82,3 +83,45 @@ async def create_memory_via_integration(

# Empty response
return {}


@router.post('/v2/integrations/{app_id}/user/facts', response_model=integration_models.EmptyResponse,
tags=['integration', 'facts'])
async def create_facts_via_integration(
request: Request,
app_id: str,
fact_data: integration_models.ExternalIntegrationCreateFact,
uid: str,
authorization: Optional[str] = Header(None)
):
# Verify API key from Authorization header
if not authorization or not authorization.startswith('Bearer '):
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header. Must be 'Bearer API_KEY'")

api_key = authorization.replace('Bearer ', '')
if not verify_api_key(app_id, api_key):
raise HTTPException(status_code=403, detail="Invalid API key")

# Verify if the app exists
app = apps_db.get_app_by_id_db(app_id)
if not app:
raise HTTPException(status_code=404, detail="App not found")

# Verify if the uid has enabled the app
enabled_plugins = redis_db.get_enabled_plugins(uid)
if app_id not in enabled_plugins:
raise HTTPException(status_code=403, detail="App is not enabled for this user")

# Check if the app has the capability external_integration > action > create_facts
if not apps_utils.app_has_action(app, 'create_facts'):
raise HTTPException(status_code=403, detail="App does not have the capability to create facts")

# Validate that text is provided
if not fact_data.text or len(fact_data.text.strip()) == 0:
raise HTTPException(status_code=422, detail="Text is required and cannot be empty")

# Process and save the fact using the utility function
process_external_integration_fact(uid, fact_data, app_id)

# Empty response
return {}
11 changes: 11 additions & 0 deletions backend/utils/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,14 @@ def app_has_action(app: App, action_name: str) -> bool:
return True

return False
def app_has_action(app: dict, action_name: str) -> bool:
"""Check if an app has a specific action capability."""
if not app.get('external_integration'):
return False

actions = app['external_integration'].get('actions', [])
for action in actions:
if action.get('action') == action_name:
return True

return False
4 changes: 2 additions & 2 deletions backend/utils/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
import os
from datetime import datetime, timezone
from typing import List, Optional
from typing import List, Optional, Tuple

import tiktoken
from langchain.schema import (
Expand All @@ -24,8 +24,8 @@
from models.transcript_segment import TranscriptSegment
from models.trend import TrendEnum, ceo_options, company_options, software_product_options, hardware_product_options, \
ai_product_options, TrendType
from utils.memories.facts import get_prompt_facts
from utils.prompts import extract_facts_prompt, extract_learnings_prompt, extract_facts_text_content_prompt
from utils.llms.fact import get_prompt_facts

llm_mini = ChatOpenAI(model='gpt-4o-mini')
llm_mini_stream = ChatOpenAI(model='gpt-4o-mini', streaming=True)
Expand Down
23 changes: 23 additions & 0 deletions backend/utils/llms/fact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List, Tuple, Optional

import database.facts as facts_db
from database.auth import get_user_name
from models.facts import Fact

def get_prompt_facts(uid: str) -> str:
user_name, user_made_facts, generated_facts = get_prompt_data(uid)
facts_str = f'you already know the following facts about {user_name}: \n{Fact.get_facts_as_str(generated_facts)}.'
if user_made_facts:
facts_str += f'\n\n{user_name} also shared the following about self: \n{Fact.get_facts_as_str(user_made_facts)}'
return user_name, facts_str + '\n'


def get_prompt_data(uid: str) -> Tuple[str, List[Fact], List[Fact]]:
# TODO: cache this
existing_facts = facts_db.get_facts(uid, limit=100)
user_made = [Fact(**fact) for fact in existing_facts if fact['manually_added']]
# TODO: filter only reviewed True
generated = [Fact(**fact) for fact in existing_facts if not fact['manually_added']]
user_name = get_user_name(uid)
# print('get_prompt_data', user_name, len(user_made), len(generated))
return user_name, user_made, generated
42 changes: 24 additions & 18 deletions backend/utils/memories/facts.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
from typing import List, Tuple
from typing import List, Tuple, Optional

import database.facts as facts_db
from database.auth import get_user_name
from models.facts import Fact
from models.facts import FactDB
from models.integrations import ExternalIntegrationCreateFact
from utils.llm import extract_facts_from_text

def process_external_integration_fact(uid: str, fact_data: ExternalIntegrationCreateFact, app_id: str) -> List[FactDB]:
fact_data.app_id = app_id

def get_prompt_facts(uid: str) -> str:
user_name, user_made_facts, generated_facts = get_prompt_data(uid)
facts_str = f'you already know the following facts about {user_name}: \n{Fact.get_facts_as_str(generated_facts)}.'
if user_made_facts:
facts_str += f'\n\n{user_name} also shared the following about self: \n{Fact.get_facts_as_str(user_made_facts)}'
return user_name, facts_str + '\n'
# Extract facts from text
extracted_facts = extract_facts_from_text(
uid,
fact_data.text,
fact_data.text_source_spec if fact_data.text_source_spec else fact_data.text_source.value
)
if not extracted_facts or len(extracted_facts) == 0:
return []

saved_facts = []

def get_prompt_data(uid: str) -> Tuple[str, List[Fact], List[Fact]]:
# TODO: cache this
existing_facts = facts_db.get_facts(uid, limit=100)
user_made = [Fact(**fact) for fact in existing_facts if fact['manually_added']]
# TODO: filter only reviewed True
generated = [Fact(**fact) for fact in existing_facts if not fact['manually_added']]
user_name = get_user_name(uid)
# print('get_prompt_data', user_name, len(user_made), len(generated))
return user_name, user_made, generated
# Save each extracted fact
for fact in extracted_facts:
fact_db = FactDB.from_fact(fact, uid, None, None)
fact_db.manually_added = False
fact_db.app_id = app_id
saved_facts.append(fact_db)
facts_db.save_facts(uid, [fact_db.dict() for fact_db in saved_facts])

return saved_facts
11 changes: 6 additions & 5 deletions backend/utils/memories/process_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
from models.task import Task, TaskStatus, TaskAction, TaskActionProvider
from models.trend import Trend
from models.notification_message import NotificationMessage
from utils.apps import get_available_apps, update_persona_prompt, sync_update_persona_prompt
from utils.llm import obtain_emotional_message, retrieve_metadata_fields_from_transcript
from utils.llm import summarize_open_glass, get_transcript_structure, generate_embedding, \
from utils.apps import get_available_apps
from utils.llm import obtain_emotional_message, retrieve_metadata_fields_from_transcript, \
summarize_open_glass, get_transcript_structure, generate_embedding, \
get_plugin_result, should_discard_memory, summarize_experience_text, new_facts_extractor, \
trends_extractor, get_email_structure, get_post_structure, get_message_structure, extract_facts_from_text, \
retrieve_metadata_from_email, retrieve_metadata_from_post, retrieve_metadata_from_message, retrieve_metadata_from_text
trends_extractor, get_email_structure, get_post_structure, get_message_structure, \
retrieve_metadata_from_email, retrieve_metadata_from_post, retrieve_metadata_from_message, retrieve_metadata_from_text, \
extract_facts_from_text
from utils.notifications import send_notification
from utils.other.hume import get_hume, HumeJobCallbackModel, HumeJobModelPredictionResponseModel
from utils.retrieval.rag import retrieve_rag_memory_context
Expand Down
Loading

0 comments on commit 69a238a

Please sign in to comment.