Amogh1221 commited on
Commit
d3ddf7e
·
verified ·
1 Parent(s): bb43d2d

Delete providers

Browse files
Files changed (46) hide show
  1. providers/__pycache__/bert_risk_signal.cpython-311.pyc +0 -0
  2. providers/__pycache__/bert_risk_signal.cpython-313.pyc +0 -0
  3. providers/__pycache__/cascade_risk_signal.cpython-311.pyc +0 -0
  4. providers/__pycache__/cascade_risk_signal.cpython-313.pyc +0 -0
  5. providers/__pycache__/chroma_DB.cpython-311.pyc +0 -0
  6. providers/__pycache__/chroma_provider.cpython-311.pyc +0 -0
  7. providers/__pycache__/chroma_vector_store.cpython-313.pyc +0 -0
  8. providers/__pycache__/clinical_risk_signal.cpython-311.pyc +0 -0
  9. providers/__pycache__/clinical_risk_signal.cpython-313.pyc +0 -0
  10. providers/__pycache__/fallback_llm_provider.cpython-313.pyc +0 -0
  11. providers/__pycache__/groq_llm_provider.cpython-313.pyc +0 -0
  12. providers/__pycache__/keyword_risk_signal.cpython-311.pyc +0 -0
  13. providers/__pycache__/keyword_risk_signal.cpython-313.pyc +0 -0
  14. providers/__pycache__/llm_risk_signal.cpython-311.pyc +0 -0
  15. providers/__pycache__/llm_risk_signal.cpython-313.pyc +0 -0
  16. providers/__pycache__/ollama_llm_provider.cpython-313.pyc +0 -0
  17. providers/__pycache__/openrouter_llm_provider.cpython-313.pyc +0 -0
  18. providers/__pycache__/pinecone_DB.cpython-311.pyc +0 -0
  19. providers/__pycache__/pinecone_provider.cpython-311.pyc +0 -0
  20. providers/__pycache__/sensevoice_stt_provider.cpython-311.pyc +0 -0
  21. providers/__pycache__/sensevoice_stt_provider.cpython-313.pyc +0 -0
  22. providers/__pycache__/speech_to_text_provider.cpython-311.pyc +0 -0
  23. providers/__pycache__/together_llm_provider.cpython-313.pyc +0 -0
  24. providers/chroma_provider.py +0 -125
  25. providers/llms/__pycache__/fallback_llm_provider.cpython-311.pyc +0 -0
  26. providers/llms/__pycache__/groq_llm_provider.cpython-311.pyc +0 -0
  27. providers/llms/__pycache__/groq_llm_provider.cpython-313.pyc +0 -0
  28. providers/llms/__pycache__/huggingface_llm_provider.cpython-311.pyc +0 -0
  29. providers/llms/__pycache__/huggingface_llm_provider.cpython-313.pyc +0 -0
  30. providers/llms/__pycache__/ollama_llm_provider.cpython-311.pyc +0 -0
  31. providers/llms/__pycache__/openrouter_llm_provider.cpython-311.pyc +0 -0
  32. providers/llms/fallback_llm_provider.py +0 -94
  33. providers/llms/groq_llm_provider.py +0 -183
  34. providers/llms/ollama_llm_provider.py +0 -214
  35. providers/pinecone_provider.py +0 -54
  36. providers/safety/__pycache__/bert.cpython-311.pyc +0 -0
  37. providers/safety/__pycache__/cascade.cpython-311.pyc +0 -0
  38. providers/safety/__pycache__/clinical.cpython-311.pyc +0 -0
  39. providers/safety/__pycache__/keyword.cpython-311.pyc +0 -0
  40. providers/safety/__pycache__/llm.cpython-311.pyc +0 -0
  41. providers/safety/bert.py +0 -58
  42. providers/safety/cascade.py +0 -25
  43. providers/safety/clinical.py +0 -17
  44. providers/safety/keyword.py +0 -102
  45. providers/safety/llm.py +0 -10
  46. providers/speech_to_text_provider.py +0 -107
providers/__pycache__/bert_risk_signal.cpython-311.pyc DELETED
Binary file (2.79 kB)
 
providers/__pycache__/bert_risk_signal.cpython-313.pyc DELETED
Binary file (2.48 kB)
 
providers/__pycache__/cascade_risk_signal.cpython-311.pyc DELETED
Binary file (1.88 kB)
 
providers/__pycache__/cascade_risk_signal.cpython-313.pyc DELETED
Binary file (1.74 kB)
 
providers/__pycache__/chroma_DB.cpython-311.pyc DELETED
Binary file (7.13 kB)
 
providers/__pycache__/chroma_provider.cpython-311.pyc DELETED
Binary file (7.14 kB)
 
providers/__pycache__/chroma_vector_store.cpython-313.pyc DELETED
Binary file (6.06 kB)
 
providers/__pycache__/clinical_risk_signal.cpython-311.pyc DELETED
Binary file (1.35 kB)
 
providers/__pycache__/clinical_risk_signal.cpython-313.pyc DELETED
Binary file (1.24 kB)
 
providers/__pycache__/fallback_llm_provider.cpython-313.pyc DELETED
Binary file (5.21 kB)
 
providers/__pycache__/groq_llm_provider.cpython-313.pyc DELETED
Binary file (9.11 kB)
 
providers/__pycache__/keyword_risk_signal.cpython-311.pyc DELETED
Binary file (4.66 kB)
 
providers/__pycache__/keyword_risk_signal.cpython-313.pyc DELETED
Binary file (4.37 kB)
 
providers/__pycache__/llm_risk_signal.cpython-311.pyc DELETED
Binary file (1.04 kB)
 
providers/__pycache__/llm_risk_signal.cpython-313.pyc DELETED
Binary file (977 Bytes)
 
providers/__pycache__/ollama_llm_provider.cpython-313.pyc DELETED
Binary file (9.78 kB)
 
providers/__pycache__/openrouter_llm_provider.cpython-313.pyc DELETED
Binary file (8.26 kB)
 
providers/__pycache__/pinecone_DB.cpython-311.pyc DELETED
Binary file (3.36 kB)
 
providers/__pycache__/pinecone_provider.cpython-311.pyc DELETED
Binary file (3.37 kB)
 
providers/__pycache__/sensevoice_stt_provider.cpython-311.pyc DELETED
Binary file (5.74 kB)
 
providers/__pycache__/sensevoice_stt_provider.cpython-313.pyc DELETED
Binary file (5.02 kB)
 
providers/__pycache__/speech_to_text_provider.cpython-311.pyc DELETED
Binary file (5.74 kB)
 
providers/__pycache__/together_llm_provider.cpython-313.pyc DELETED
Binary file (8.05 kB)
 
providers/chroma_provider.py DELETED
@@ -1,125 +0,0 @@
1
- import os
2
- from langchain_chroma import Chroma
3
- from langchain_ollama import OllamaEmbeddings
4
- from langchain_community.retrievers import BM25Retriever
5
- from langchain_classic.retrievers import EnsembleRetriever
6
-
7
- class ChromaVectorStore:
8
- """
9
- Concrete implementation of the VectorStore protocol using ChromaDB.
10
- Handles semantic embedding generation and hybrid retrieval (BM25 + MMR)
11
- for pulling clinical context during background analysis.
12
- """
13
- def __init__(self, host: str, embedding_model: str, persist_dir: str, collection_name: str):
14
- self.embeddings = OllamaEmbeddings(
15
- model=embedding_model,
16
- base_url=host,
17
- )
18
-
19
- self.vectorstore = Chroma(
20
- collection_name=collection_name,
21
- embedding_function=self.embeddings,
22
- persist_directory=persist_dir,
23
- )
24
- count = self.vectorstore._collection.count()
25
- if count == 0:
26
- print(
27
- "[RAG WARNING] ChromaDB collection is empty. "
28
- "Run `python build_vectorDB.py` to populate it before starting a session."
29
- )
30
- self.ensemble_retriever = None
31
- else:
32
- print("[RAG] Building BM25 index for Hybrid Retrieval...")
33
- import pickle
34
- bm25_cache_path = os.path.join(persist_dir, "bm25_index.pkl")
35
-
36
- bm25_retriever = None
37
- if os.path.exists(bm25_cache_path):
38
- print("[RAG] Loading BM25 index from local cache...")
39
- try:
40
- with open(bm25_cache_path, "rb") as f:
41
- bm25_retriever = pickle.load(f)
42
- except Exception as e:
43
- print(f"[RAG] Failed to load BM25 cache: {e}. Rebuilding...")
44
- bm25_retriever = None
45
-
46
- if bm25_retriever is None:
47
- print("[RAG] Building BM25 index from ChromaDB (this may take a moment for large databases)...")
48
- # Extract all stored documents in batches to avoid SQLite 'too many SQL variables' error
49
- documents = []
50
- batch_size = 5000
51
- offset = 0
52
- while True:
53
- batch = self.vectorstore.get(limit=batch_size, offset=offset)
54
- batch_docs = batch.get("documents", [])
55
- if not batch_docs:
56
- break
57
- documents.extend(batch_docs)
58
- offset += batch_size
59
-
60
- if documents:
61
- bm25_retriever = BM25Retriever.from_texts(documents)
62
- bm25_retriever.k = min(8, len(documents))
63
-
64
- # Save to cache for future restarts
65
- try:
66
- with open(bm25_cache_path, "wb") as f:
67
- pickle.dump(bm25_retriever, f)
68
- print("[RAG] BM25 index cached successfully.")
69
- except Exception as e:
70
- print(f"[RAG WARNING] Failed to cache BM25 index: {e}")
71
-
72
- if bm25_retriever:
73
- # 2. Semantic Retriever (Chroma MMR)
74
- mmr_retriever = self.vectorstore.as_retriever(
75
- search_type="mmr",
76
- search_kwargs={
77
- "k": bm25_retriever.k,
78
- "fetch_k": max(20, bm25_retriever.k * 3),
79
- "lambda_mult": 0.7
80
- }
81
- )
82
-
83
- # 3. Hybrid Ensemble (50% Lexical, 50% Semantic)
84
- self.ensemble_retriever = EnsembleRetriever(
85
- retrievers=[bm25_retriever, mmr_retriever],
86
- weights=[0.5, 0.5]
87
- )
88
- else:
89
- self.ensemble_retriever = None
90
-
91
- def retrieve(self, query: str, k: int = 8) -> str:
92
- if not query or not query.strip():
93
- return ""
94
-
95
- try:
96
- if self.ensemble_retriever:
97
- results = self.ensemble_retriever.invoke(query)
98
- # EnsembleRetriever returns a list of Document objects
99
- # We limit the final combined output to the top k matches
100
- return "\n\n".join([doc.page_content for doc in results[:k]])
101
-
102
- # Fallback if ensemble isn't ready (e.g. empty DB)
103
- count = self.vectorstore._collection.count()
104
- if count == 0:
105
- return ""
106
-
107
- actual_fetch_k = min(30, count)
108
- actual_k = min(k, actual_fetch_k)
109
-
110
- results = self.vectorstore.max_marginal_relevance_search(
111
- query,
112
- k=actual_k,
113
- fetch_k=actual_fetch_k,
114
- lambda_mult=0.7,
115
- )
116
- return "\n\n".join([doc.page_content for doc in results])
117
-
118
- except Exception as e:
119
- print(f"[RAG ERROR] Hybrid search failed: {e}. Attempting similarity fallback.")
120
- try:
121
- results = self.vectorstore.similarity_search(query, k=k)
122
- return "\n\n".join([doc.page_content for doc in results])
123
- except Exception as e2:
124
- print(f"[RAG ERROR] Similarity fallback also failed: {e2}")
125
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/llms/__pycache__/fallback_llm_provider.cpython-311.pyc DELETED
Binary file (5.79 kB)
 
providers/llms/__pycache__/groq_llm_provider.cpython-311.pyc DELETED
Binary file (9.86 kB)
 
providers/llms/__pycache__/groq_llm_provider.cpython-313.pyc DELETED
Binary file (8.03 kB)
 
providers/llms/__pycache__/huggingface_llm_provider.cpython-311.pyc DELETED
Binary file (8.67 kB)
 
providers/llms/__pycache__/huggingface_llm_provider.cpython-313.pyc DELETED
Binary file (8.32 kB)
 
providers/llms/__pycache__/ollama_llm_provider.cpython-311.pyc DELETED
Binary file (12.3 kB)
 
providers/llms/__pycache__/openrouter_llm_provider.cpython-311.pyc DELETED
Binary file (8.62 kB)
 
providers/llms/fallback_llm_provider.py DELETED
@@ -1,94 +0,0 @@
1
- from core.ports import LLM1Output, LLM2Output, LLM3Output
2
-
3
- class FallbackLLMProvider:
4
- """
5
- Composite LLM Provider that loops through a list of other providers.
6
- If a provider throws an exception (e.g. rate limit / 429), it tries the next.
7
- Returns safe fallback strings if all providers fail.
8
- """
9
- def __init__(self, providers: list):
10
- self.providers = providers
11
-
12
- def generate_opening_context(self, profile_recap: str | None) -> list:
13
- # This doesn't call an LLM directly, so we just use the first provider's logic
14
- return self.providers[0].generate_opening_context(profile_recap)
15
-
16
- def psychiatrist_response(self, context: list, patient_info: dict = None) -> LLM1Output:
17
- for provider in self.providers:
18
- try:
19
- return provider.psychiatrist_response(context, patient_info)
20
- except Exception as e:
21
- print(f"[FallbackRouter] Provider {type(provider).__name__} failed for LLM1: {e}")
22
- continue
23
-
24
- # All failed
25
- print("[FallbackRouter] ALL providers failed for LLM1!")
26
- return LLM1Output(
27
- assistant_message="I'm here, and I'm listening. Please take your time. && Would you like to tell me more about what's on your mind?",
28
- intent="CONTINUE",
29
- risk_flag=False,
30
- clinical_summary=None
31
- )
32
-
33
- def internal_reasoning(self, context: list) -> LLM2Output:
34
- for provider in self.providers:
35
- try:
36
- return provider.internal_reasoning(context)
37
- except Exception as e:
38
- print(f"[FallbackRouter] Provider {type(provider).__name__} failed for LLM2: {e}")
39
- continue
40
-
41
- # All failed
42
- print("[FallbackRouter] ALL providers failed for LLM2!")
43
- return LLM2Output(
44
- assistant_message="I'm here, taking everything in. Please go on.",
45
- emotional_themes=[],
46
- thinking_patterns=[],
47
- behavioral_patterns=[],
48
- interpersonal_dynamics=[],
49
- stressors=[],
50
- unclear_areas=[],
51
- risk_assessment="Unable to complete risk assessment due to analysis error.",
52
- protective_factors=[]
53
- )
54
-
55
- def psychiatrist_query_response(self, context: list, retrieved_context: str) -> str:
56
- for provider in self.providers:
57
- try:
58
- return provider.psychiatrist_query_response(context, retrieved_context)
59
- except Exception as e:
60
- print(f"[FallbackRouter] Provider {type(provider).__name__} failed for Query: {e}")
61
- continue
62
-
63
- return "I hear you, and we will find a way through this together. Let's keep exploring what might help."
64
-
65
- def generate_end_of_session_profile(self, old_profile: dict, session_history: list, patient_info: dict = None) -> LLM3Output:
66
- for provider in self.providers:
67
- try:
68
- return provider.generate_end_of_session_profile(old_profile, session_history, patient_info)
69
- except Exception as e:
70
- print(f"[FallbackRouter] Provider {type(provider).__name__} failed for LLM3: {e}")
71
- continue
72
-
73
- return LLM3Output(
74
- session_summary="Error generating session summary.",
75
- update_profile=False,
76
- emotional_themes=[],
77
- thinking_patterns=[],
78
- behavioral_patterns=[],
79
- interpersonal_dynamics=[],
80
- stressors=[],
81
- unclear_areas=[],
82
- risk_assessment="Error.",
83
- protective_factors=[]
84
- )
85
-
86
- def summarize_history(self, turns: list) -> str:
87
- for provider in self.providers:
88
- try:
89
- return provider.summarize_history(turns)
90
- except Exception as e:
91
- print(f"[FallbackRouter] Provider {type(provider).__name__} failed for Summarize: {e}")
92
- continue
93
-
94
- return "[Summary unavailable]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/llms/groq_llm_provider.py DELETED
@@ -1,183 +0,0 @@
1
- import os
2
- import json
3
- from groq import Groq
4
- from core.ports import LLM1Output, LLM2Output, LLM3Output
5
- from core.prompts import LLM1_SYSTEM_PROMPT, LLM2_SYSTEM_PROMPT, LLM3_SYSTEM_PROMPT
6
-
7
- class GroqLLMProvider:
8
- """
9
- Concrete implementation of the LLMProvider protocol using the Groq API.
10
- Provides true asynchronous concurrency (no local GPU threading lock required).
11
- """
12
- def __init__(self, model1: str, model2: str):
13
- self.model1 = model1
14
- self.model2 = model2
15
- # Relies on the GROQ_API_KEY environment variable being set
16
- self.client = Groq()
17
-
18
- def generate_opening_context(self, profile_recap: str | None) -> list:
19
- if profile_recap:
20
- return [
21
- {
22
- "role": "user",
23
- "content": (
24
- f"{profile_recap}\n\n"
25
- "Start the psychiatric session. Greet the patient warmly, "
26
- "briefly acknowledge continuity if it feels right, then use && "
27
- "to add an open question at the end — e.g. "
28
- "'Welcome back, it's good to see you. && How have things been for you since we last spoke?'"
29
- ),
30
- }
31
- ]
32
- else:
33
- return [
34
- {
35
- "role": "user",
36
- "content": (
37
- "Start the psychiatric session. Greet the patient warmly and naturally, "
38
- "then use && to add an open question at the end — e.g. "
39
- "'Hello, I'm Dr. Aiden. I'm here to listen. && What brings you here today?'"
40
- ),
41
- }
42
- ]
43
-
44
- def psychiatrist_response(self, context: list, patient_info: dict = None) -> LLM1Output:
45
- try:
46
- sys_prompt = LLM1_SYSTEM_PROMPT
47
- if patient_info:
48
- demographics = (
49
- f"PATIENT DEMOGRAPHICS:\n"
50
- f"- Name: {patient_info.get('name', 'Unknown')}\n"
51
- f"- Age: {patient_info.get('age', 'Unknown')}\n"
52
- f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
53
- f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
54
- f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
55
- )
56
- sys_prompt = demographics + sys_prompt
57
-
58
- messages = [{"role": "system", "content": sys_prompt}]
59
- for m in context:
60
- messages.append({"role": m["role"], "content": m["content"]})
61
-
62
- response = self.client.chat.completions.create(
63
- model=self.model1,
64
- messages=messages,
65
- response_format={"type": "json_object"},
66
- temperature=0.6,
67
- max_completion_tokens=1024,
68
- )
69
- raw = response.choices[0].message.content
70
- return LLM1Output.model_validate_json(raw)
71
- except Exception as e:
72
- print(f"[Groq LLM1 ERROR] {e}")
73
- raise e
74
-
75
- def internal_reasoning(self, context: list) -> LLM2Output:
76
- try:
77
- messages = [{"role": "system", "content": LLM2_SYSTEM_PROMPT}]
78
- for m in context:
79
- messages.append({"role": m["role"], "content": m["content"]})
80
-
81
- response = self.client.chat.completions.create(
82
- model=self.model2,
83
- messages=messages,
84
- response_format={"type": "json_object"},
85
- temperature=0.1,
86
- max_completion_tokens=1024,
87
- )
88
- raw = response.choices[0].message.content
89
- return LLM2Output.model_validate_json(raw)
90
- except Exception as e:
91
- print(f"[Groq LLM2 ERROR] {e}")
92
- raise e
93
-
94
- def psychiatrist_query_response(self, context: list, retrieved_context: str) -> str:
95
- try:
96
- sys_prompt = (
97
- "You are a compassionate AI psychiatrist. The patient has asked for advice. "
98
- "Synthesize a thoughtful response using the following clinical guidelines:\n\n"
99
- f"{retrieved_context}\n\n"
100
- "Keep your response empathetic, natural, and under 4 sentences. Do not use markdown."
101
- )
102
- messages = [{"role": "system", "content": sys_prompt}]
103
- for m in context:
104
- messages.append({"role": m["role"], "content": m["content"]})
105
-
106
- response = self.client.chat.completions.create(
107
- model=self.model1,
108
- messages=messages,
109
- temperature=0.5,
110
- max_completion_tokens=512,
111
- )
112
- return response.choices[0].message.content.strip()
113
- except Exception as e:
114
- print(f"[Groq QUERY ERROR] {e}")
115
- raise e
116
-
117
- def generate_end_of_session_profile(self, old_profile: dict, session_history: list, patient_info: dict = None) -> LLM3Output:
118
- try:
119
- sys_prompt = LLM3_SYSTEM_PROMPT
120
- if patient_info:
121
- demographics = (
122
- f"PATIENT DEMOGRAPHICS:\n"
123
- f"- Name: {patient_info.get('name', 'Unknown')}\n"
124
- f"- Age: {patient_info.get('age', 'Unknown')}\n"
125
- f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
126
- f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
127
- f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
128
- )
129
- sys_prompt = demographics + sys_prompt
130
-
131
- formatted_history = "\n".join(f"{t['role'].upper()}: {t['content']}" for t in session_history)
132
- user_prompt = (
133
- f"EXISTING PROFILE:\n{old_profile}\n\n"
134
- f"RECENT SESSION TRANSCRIPT:\n{formatted_history}\n\n"
135
- "Please generate the 100-200 word session_summary and the merged, deduplicated clinical profile."
136
- )
137
- messages = [
138
- {"role": "system", "content": sys_prompt},
139
- {"role": "user", "content": user_prompt}
140
- ]
141
-
142
- response = self.client.chat.completions.create(
143
- model=self.model1,
144
- messages=messages,
145
- response_format={"type": "json_object"},
146
- temperature=0.2,
147
- max_completion_tokens=1500,
148
- )
149
- raw = response.choices[0].message.content
150
- return LLM3Output.model_validate_json(raw)
151
- except Exception as e:
152
- print(f"[Groq LLM3 ERROR] {e}")
153
- raise e
154
-
155
- def summarize_history(self, turns: list) -> str:
156
- if not turns:
157
- return ""
158
- try:
159
- formatted = "\n".join(
160
- f"{t['role'].upper()}: {t['content']}" for t in turns
161
- )
162
- messages = [
163
- {
164
- "role": "system",
165
- "content": (
166
- "You are a clinical note-taker. Summarize the following "
167
- "patient-psychiatrist conversation excerpt in 3-5 sentences, "
168
- "third-person clinical register. Capture key symptoms, "
169
- "themes, and any safety concerns. Be concise."
170
- ),
171
- },
172
- {"role": "user", "content": formatted},
173
- ]
174
- response = self.client.chat.completions.create(
175
- model=self.model1,
176
- messages=messages,
177
- temperature=0.3,
178
- max_completion_tokens=300,
179
- )
180
- return response.choices[0].message.content.strip()
181
- except Exception as e:
182
- print(f"[Groq SUMMARIZE ERROR] {e}")
183
- raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/llms/ollama_llm_provider.py DELETED
@@ -1,214 +0,0 @@
1
- import os
2
- import threading
3
- import ollama
4
- from core.ports import LLM1Output, LLM2Output, LLM3Output
5
- from core.prompts import LLM1_SYSTEM_PROMPT, LLM2_SYSTEM_PROMPT, LLM3_SYSTEM_PROMPT
6
-
7
- class OllamaLLMProvider:
8
- """
9
- Concrete implementation of the LLMProvider protocol using local Ollama models.
10
- Provides the engine for both the fast conversational responses (LLM1)
11
- and the deep background pattern analysis (LLM2).
12
- """
13
- def __init__(self, host: str, model1: str, model2: str):
14
- self.host = host
15
- self.model1 = model1
16
- self.model2 = model2
17
- # Global lock to queue requests and prevent CUDA out-of-memory errors
18
- self._llm_lock = threading.Lock()
19
-
20
- def _client(self):
21
- return ollama.Client(host=self.host)
22
-
23
- def generate_opening_context(self, profile_recap: str | None) -> list:
24
- if profile_recap:
25
- return [
26
- {
27
- "role": "user",
28
- "content": (
29
- f"{profile_recap}\n\n"
30
- "Start the psychiatric session. Greet the patient warmly, "
31
- "briefly acknowledge continuity if it feels right, then use && "
32
- "to add an open question at the end — e.g. "
33
- "'Welcome back, it's good to see you. && How have things been for you since we last spoke?'"
34
- ),
35
- }
36
- ]
37
- else:
38
- return [
39
- {
40
- "role": "user",
41
- "content": (
42
- "Start the psychiatric session. Greet the patient warmly and naturally, "
43
- "then use && to add an open question at the end — e.g. "
44
- "'Hello, I'm Dr. Aiden. I'm here to listen. && What brings you here today?'"
45
- ),
46
- }
47
- ]
48
-
49
- def psychiatrist_response(self, context: list, patient_info: dict = None) -> LLM1Output:
50
- try:
51
- sys_prompt = LLM1_SYSTEM_PROMPT
52
- if patient_info:
53
- demographics = (
54
- f"PATIENT DEMOGRAPHICS:\n"
55
- f"- Name: {patient_info.get('name', 'Unknown')}\n"
56
- f"- Age: {patient_info.get('age', 'Unknown')}\n"
57
- f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
58
- f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
59
- f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
60
- )
61
- sys_prompt = demographics + sys_prompt
62
-
63
- messages = [{"role": "system", "content": sys_prompt}]
64
- for m in context:
65
- messages.append({"role": m["role"], "content": m["content"]})
66
-
67
- with self._llm_lock:
68
- response = self._client().chat(
69
- model=self.model1,
70
- messages=messages,
71
- format=LLM1Output.model_json_schema(),
72
- options={"temperature": 0.6, "num_predict": 1024, "num_ctx": 8192},
73
- )
74
- raw = response.message.content
75
- return LLM1Output.model_validate_json(raw)
76
- except Exception as e:
77
- print(f"[LLM1 ERROR] {e}")
78
- return LLM1Output(
79
- assistant_message="I'm here, and I'm listening. Please take your time. && Would you like to tell me more about what's on your mind?",
80
- intent="CONTINUE",
81
- risk_flag=False,
82
- clinical_summary=None
83
- )
84
-
85
- def internal_reasoning(self, context: list) -> LLM2Output:
86
- try:
87
- messages = [{"role": "system", "content": LLM2_SYSTEM_PROMPT}]
88
- for m in context:
89
- messages.append({"role": m["role"], "content": m["content"]})
90
-
91
- with self._llm_lock:
92
- response = self._client().chat(
93
- model=self.model2,
94
- messages=messages,
95
- format=LLM2Output.model_json_schema(),
96
- options={"temperature": 0.1, "num_predict": 1024, "num_ctx": 8192},
97
- keep_alive=0,
98
- )
99
- raw = response.message.content
100
- return LLM2Output.model_validate_json(raw)
101
- except Exception as e:
102
- print(f"[LLM2 ERROR] {e}")
103
- return LLM2Output(
104
- assistant_message="I'm here, taking everything in. Please go on.",
105
- emotional_themes=[],
106
- thinking_patterns=[],
107
- behavioral_patterns=[],
108
- interpersonal_dynamics=[],
109
- stressors=[],
110
- unclear_areas=[],
111
- risk_assessment="Unable to complete risk assessment due to analysis error.",
112
- protective_factors=[]
113
- )
114
-
115
- def psychiatrist_query_response(self, context: list, retrieved_context: str) -> str:
116
- try:
117
- sys_prompt = (
118
- "You are a compassionate AI psychiatrist. The patient has asked for advice. "
119
- "Synthesize a thoughtful response using the following clinical guidelines:\n\n"
120
- f"{retrieved_context}\n\n"
121
- "Keep your response empathetic, natural, and under 4 sentences. Do not use markdown."
122
- )
123
- messages = [{"role": "system", "content": sys_prompt}]
124
- for m in context:
125
- messages.append({"role": m["role"], "content": m["content"]})
126
-
127
- with self._llm_lock:
128
- response = self._client().chat(
129
- model=self.model1,
130
- messages=messages,
131
- options={"temperature": 0.5, "num_predict": 512, "num_ctx": 8192},
132
- )
133
- return response.message.content.strip()
134
- except Exception as e:
135
- print(f"[QUERY ERROR] {e}")
136
- return "I hear you, and we will find a way through this together. Let's keep exploring what might help."
137
-
138
- def generate_end_of_session_profile(self, old_profile: dict, session_history: list, patient_info: dict = None) -> LLM3Output:
139
- try:
140
- sys_prompt = LLM3_SYSTEM_PROMPT
141
- if patient_info:
142
- demographics = (
143
- f"PATIENT DEMOGRAPHICS:\n"
144
- f"- Name: {patient_info.get('name', 'Unknown')}\n"
145
- f"- Age: {patient_info.get('age', 'Unknown')}\n"
146
- f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
147
- f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
148
- f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
149
- )
150
- sys_prompt = demographics + sys_prompt
151
-
152
- formatted_history = "\n".join(f"{t['role'].upper()}: {t['content']}" for t in session_history)
153
- user_prompt = (
154
- f"EXISTING PROFILE:\n{old_profile}\n\n"
155
- f"RECENT SESSION TRANSCRIPT:\n{formatted_history}\n\n"
156
- "Please generate the 100-200 word session_summary and the merged, deduplicated clinical profile."
157
- )
158
- messages = [
159
- {"role": "system", "content": sys_prompt},
160
- {"role": "user", "content": user_prompt}
161
- ]
162
-
163
- with self._llm_lock:
164
- response = self._client().chat(
165
- model=self.model1,
166
- messages=messages,
167
- format=LLM3Output.model_json_schema(),
168
- options={"temperature": 0.2, "num_predict": 1500, "num_ctx": 8192},
169
- )
170
- raw = response.message.content
171
- return LLM3Output.model_validate_json(raw)
172
- except Exception as e:
173
- print(f"[LLM3 ERROR] {e}")
174
- return LLM3Output(
175
- session_summary="Error generating session summary.",
176
- emotional_themes=[],
177
- thinking_patterns=[],
178
- behavioral_patterns=[],
179
- interpersonal_dynamics=[],
180
- stressors=[],
181
- unclear_areas=[],
182
- risk_assessment="Error.",
183
- protective_factors=[]
184
- )
185
-
186
- def summarize_history(self, turns: list) -> str:
187
- if not turns:
188
- return ""
189
- try:
190
- formatted = "\n".join(
191
- f"{t['role'].upper()}: {t['content']}" for t in turns
192
- )
193
- messages = [
194
- {
195
- "role": "system",
196
- "content": (
197
- "You are a clinical note-taker. Summarize the following "
198
- "patient-psychiatrist conversation excerpt in 3-5 sentences, "
199
- "third-person clinical register. Capture key symptoms, "
200
- "themes, and any safety concerns. Be concise."
201
- ),
202
- },
203
- {"role": "user", "content": formatted},
204
- ]
205
- with self._llm_lock:
206
- response = self._client().chat(
207
- model=self.model1,
208
- messages=messages,
209
- options={"temperature": 0.3, "num_predict": 300, "num_ctx": 8192},
210
- )
211
- return response.message.content.strip()
212
- except Exception as e:
213
- print(f"[SUMMARIZE ERROR] {e}")
214
- return "[Summary unavailable]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/pinecone_provider.py DELETED
@@ -1,54 +0,0 @@
1
- import os
2
- from langchain_pinecone import PineconeVectorStore as LangchainPinecone
3
- from langchain_ollama import OllamaEmbeddings
4
- from pinecone import Pinecone
5
-
6
- class PineconeVectorStore:
7
- """
8
- Concrete implementation of the VectorStore protocol using Pinecone.
9
- Handles semantic embedding generation and retrieval for pulling clinical context.
10
- """
11
- def __init__(self, host: str, embedding_model: str, index_name: str, api_key: str):
12
- self.embeddings = OllamaEmbeddings(
13
- model=embedding_model,
14
- base_url=host,
15
- )
16
-
17
- # Initialize the Pinecone SDK client
18
- self.pc = Pinecone(api_key=api_key)
19
-
20
- # We assume the index already exists.
21
- # If it doesn't, this will throw an error when accessed.
22
- self.index = self.pc.Index(index_name)
23
-
24
- self.vectorstore = LangchainPinecone(
25
- index=self.index,
26
- embedding=self.embeddings,
27
- text_key="text" # The default text metadata key used by langchain
28
- )
29
-
30
- # Note: Pinecone doesn't naturally support local BM25/hybrid search
31
- # in the exact same way as local Chroma without serverless Pinecone BM25 integrations.
32
- # We will use pure semantic search (Max Marginal Relevance or Similarity).
33
-
34
- def retrieve(self, query: str, k: int = 8) -> str:
35
- if not query or not query.strip():
36
- return ""
37
-
38
- try:
39
- # First try Max Marginal Relevance search for diverse results
40
- results = self.vectorstore.max_marginal_relevance_search(
41
- query,
42
- k=k,
43
- fetch_k=max(20, k * 3),
44
- lambda_mult=0.7,
45
- )
46
- return "\n\n".join([doc.page_content for doc in results])
47
- except Exception as e:
48
- print(f"[RAG ERROR] MMR search failed in Pinecone: {e}. Attempting standard similarity fallback.")
49
- try:
50
- results = self.vectorstore.similarity_search(query, k=k)
51
- return "\n\n".join([doc.page_content for doc in results])
52
- except Exception as e2:
53
- print(f"[RAG ERROR] Similarity fallback also failed in Pinecone: {e2}")
54
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/safety/__pycache__/bert.cpython-311.pyc DELETED
Binary file (3.27 kB)
 
providers/safety/__pycache__/cascade.cpython-311.pyc DELETED
Binary file (1.88 kB)
 
providers/safety/__pycache__/clinical.cpython-311.pyc DELETED
Binary file (1.34 kB)
 
providers/safety/__pycache__/keyword.cpython-311.pyc DELETED
Binary file (4.66 kB)
 
providers/safety/__pycache__/llm.cpython-311.pyc DELETED
Binary file (1.04 kB)
 
providers/safety/bert.py DELETED
@@ -1,58 +0,0 @@
1
- from typing import Optional
2
- from transformers import pipeline
3
- import logging
4
-
5
- from core.ports import LLM1Output, LLM2Output
6
-
7
- logger = logging.getLogger(__name__)
8
-
9
- class BertRiskSignal:
10
- """
11
- NLP-based risk signal using a fine-tuned BERT model.
12
- Downloads and caches the model on initial run to the HF_HOME directory.
13
- Checks the patient's raw message for suicidal/self-harm ideation
14
- with much higher contextual accuracy than regex keywords.
15
- """
16
- def __init__(self, model_name: str = "Akashpaul123/bert-suicide-detection"):
17
- logger.info(f"Loading BertRiskSignal with model: {model_name}.")
18
- # Use local_files_only=True to skip HuggingFace's network version-check on startup.
19
- # This prevents 504 timeouts when the hub is unreachable or slow.
20
- # On first run (no cache), this will fail and fall back to downloading automatically.
21
- try:
22
- self.classifier = pipeline(
23
- "text-classification",
24
- model=model_name,
25
- truncation=True,
26
- max_length=512,
27
- local_files_only=True,
28
- )
29
- logger.info("BertRiskSignal loaded from local cache.")
30
- except Exception:
31
- logger.info("Local cache not found. Downloading BERT model from HuggingFace (first-time setup)...")
32
- self.classifier = pipeline(
33
- "text-classification",
34
- model=model_name,
35
- truncation=True,
36
- max_length=512,
37
- )
38
- logger.info("BertRiskSignal downloaded and loaded successfully.")
39
-
40
- def check(self, message: str, llm1_output: LLM1Output, llm2_output: Optional[LLM2Output] = None) -> bool:
41
- if not message or not message.strip():
42
- return False
43
-
44
- try:
45
- result = self.classifier(message)
46
- label = result[0]['label'].lower()
47
- score = result[0]['score']
48
-
49
- logger.info(f"BERT raw output for '{message}': {label} (score: {score:.3f})")
50
-
51
- # Outputs 'label_1' for suicide/risk & 'label_0' for non-suicide
52
- if "label_1" in label:
53
- return True
54
-
55
- return False
56
- except Exception as e:
57
- logger.error(f"Error in BertRiskSignal: {e}")
58
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/safety/cascade.py DELETED
@@ -1,25 +0,0 @@
1
- from typing import Optional
2
- from core.ports import RiskSignal, LLM1Output, LLM2Output
3
- import logging
4
-
5
- logger = logging.getLogger(__name__)
6
-
7
- class CascadeRiskSignal:
8
- """
9
- Implements a Filter-Verify (Cascade) architecture for risk signals.
10
- Passes the message through a high-speed, high-recall filter first.
11
- Only if the filter triggers does it pass the message to a slower, high-precision verifier.
12
- """
13
- def __init__(self, filter_signal: RiskSignal, verify_signal: RiskSignal):
14
- self.filter_signal = filter_signal
15
- self.verify_signal = verify_signal
16
-
17
- def check(self, message: str, llm1_output: LLM1Output, llm2_output: Optional[LLM2Output] = None) -> bool:
18
- # 1. FAST PASS
19
- if not self.filter_signal.check(message, llm1_output, llm2_output):
20
- return False
21
-
22
- logger.info("Fast-pass filter triggered. Verifying with secondary signal...")
23
-
24
- # 2. SLOW PASS (Verification)
25
- return self.verify_signal.check(message, llm1_output, llm2_output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/safety/clinical.py DELETED
@@ -1,17 +0,0 @@
1
- from typing import Optional
2
- from core.ports import LLM1Output, LLM2Output
3
-
4
- class ClinicalRiskSignal:
5
- """
6
- Risk signal that triggers based on the 'risk_assessment' output from the
7
- background LLM2 pattern analysis.
8
- """
9
- def check(self, message: str, llm1_output: LLM1Output, llm2_output: Optional[LLM2Output] = None) -> bool:
10
- if llm2_output is None:
11
- return False
12
- assessment = llm2_output.risk_assessment.strip().lower()
13
- if assessment.startswith("no safety concerns identified"):
14
- return False
15
- if assessment == "":
16
- return False
17
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/safety/keyword.py DELETED
@@ -1,102 +0,0 @@
1
- import re
2
- from typing import Optional
3
- from core.ports import LLM1Output, LLM2Output
4
-
5
- # ---------------------------------------------------------------------------
6
- # Deterministic, LLM-independent risk patterns.
7
- # Biased toward false positives — the cost of over-triggering (showing crisis
8
- # resources unnecessarily) is low; the cost of missing a real signal is high.
9
- #
10
- # This list was built and tested against a real conversation log where the
11
- # LLM-only signal (risk_flag) missed several explicit statements across
12
- # consecutive turns. Keeping this signal independent of any LLM call means
13
- # it can't fail for the same reason the model failed.
14
- # ---------------------------------------------------------------------------
15
- _RISK_PATTERNS = [
16
- # --- Direct desire to die / end one's life ---
17
- r"\b(want|wanted|wanna|going to|gonna|thinking about|think about|thought about|considering|considered|planning to|plan to)\b.{0,40}\b(kill|end|take)\b.{0,20}\b(myself|my life|it all)\b",
18
- r"\b(want|wanted|wanna|going to|gonna)\b.{0,15}\b(die|death)\b", # "wanna die", "going to die"
19
- r"\b(end(ing)? it all)\b",
20
- r"\b(suicid(e|al)|self.harm|self-harm|selfharm)\b",
21
- r"\b(don'?t want to (be here|live|exist|wake up))\b",
22
- r"\b(no (reason|point|purpose) (to|in) (live|living|go on|continue))\b",
23
- r"\b(better off (dead|without me|if i (were|was) gone))\b",
24
- r"\b(end (my life|everything|my pain|my suffering))\b",
25
- r"\b(can'?t (go on|take it|do this) anymore)\b",
26
- r"\b(wish(ing)? (i was|i were|to be) dead\b)",
27
- r"\b(plan(ning)? to (die|kill|hurt) (myself|my self))\b",
28
-
29
- # --- Specific means / methods, standalone — do NOT require a lead-in verb
30
- # since people phrase these many ways ---
31
- r"\b(kill(ing)?|hang(ing)?|hung)\b.{0,10}\bmyself\b",
32
- r"\bjump(ing)?\s+(off|from)\b",
33
- r"\b(cut(ting)?|burn(ing)?|starv(e|ing)|hitt?ing|punch(ing)?)\b.{0,10}\b(myself|my self)\b",
34
- r"\bhurt(ing)?\s+(myself|my self)\b",
35
- r"\boverdose|od'?ing\b",
36
- r"\bswallow(ing)?\s+(all\s+)?(the\s+)?pills\b",
37
-
38
- # --- Passive ideation / hopelessness ---
39
- r"\bwhat(’|'|)s?\s+the point( of (living|anything|life))?\b",
40
- r"\b(no (hope|future|reason to (stay|continue|keep going)))\b",
41
- r"\b(life (is|isn'?t) worth (living|it))\b",
42
- r"\b(just want (to disappear|it to end|to stop existing))\b",
43
- r"\b(everyone (would be|is) better off without me)\b",
44
- r"\b(giving|given|gave|give)\s+up\s+on\s+(life|everything)\b", # any tense, requires "on life/everything" to cut false positives like "gave up my seat"
45
- r"\b(too (tired|exhausted) to (go on|continue|fight))\b",
46
-
47
- # --- Self-harm urges ---
48
- r"\b(urge(s)? to (hurt|harm|cut|burn))\b",
49
-
50
- # --- Explicit direct statements ---
51
- r"\bi (don'?t|do not) (want|deserve) to (live|be alive)\b",
52
- r"\b(thinking about|thought about) (dying|death|not being here|ending it)\b",
53
-
54
- # --- Farewell / final-arrangement language — a distinct warning-sign
55
- # category in clinical suicide-risk screening (alongside direct
56
- # ideation), not just another phrasing of "I want to die" ---
57
- r"\b(this is (my )?goodbye|saying goodbye|say my goodbyes)\b",
58
- r"\b(won'?t|wont) be (around|here) (much longer|for long|anymore)\b",
59
- r"\b(take care of|look after) (my|the) (dog|cat|kids|children|family)\b.{0,40}\b(when|after) i'?m gone\b",
60
- r"\bnot (going to|gonna) be (a problem|around) (for )?much longer\b",
61
- r"\bwon'?t (have to|need to) worry about me (much longer|soon|anymore)\b",
62
-
63
- # --- Burden language — very common phrasing in real disclosures ---
64
- r"\b(such a |such)?burden (to|on) (everyone|my family|everybody|people)\b",
65
- r"\btired of being a burden\b",
66
-
67
- # --- Evasive slang / obfuscated spellings, used specifically to get
68
- # past literal-word filters — worth matching for exactly that reason ---
69
- r"\b(kms|kys)\b",
70
- r"\bunaliv(e|ing|ed)\b",
71
- r"\bsewerslide\b", # documented deliberate misspelling of "suicide"
72
-
73
- # --- Planning / preparation and access to means, kept at the level of
74
- # intent-language rather than naming specific objects, so this adds
75
- # coverage without turning into a methods list ---
76
- r"\b(have a plan|got a plan)\b",
77
- r"\b(pills|gun|rope|knife)\s+ready\b",
78
- r"\b(have|got|access to) (the means|a way) to (do it|end (it|things|my life))\b",
79
- ]
80
-
81
- _COMPILED = [re.compile(p, re.IGNORECASE | re.DOTALL) for p in _RISK_PATTERNS]
82
-
83
-
84
- class KeywordRiskSignal:
85
- """
86
- Deterministic, regex-based risk signal. Runs independently of any LLM —
87
- checks only the raw patient message text. This is intentionally the
88
- fastest and most "dumb" signal in the RiskAssessmentService: it can't be
89
- talked out of firing, doesn't depend on model quality or prompt
90
- adherence, and doesn't require a network call.
91
-
92
- Known limitation (accepted, not a bug): purely contextual pleas — e.g.
93
- "don't you think you should stop me by helping me?" — carry no risk
94
- vocabulary in isolation and won't match here. That class of statement is
95
- exactly what LLMRiskSignal (which sees conversation history) is for.
96
- Treat the two signals as complementary, not redundant.
97
- """
98
-
99
- def check(self, message: str, llm1_output: LLM1Output, llm2_output: Optional[LLM2Output] = None) -> bool:
100
- if not message or not message.strip():
101
- return False
102
- return any(pattern.search(message) for pattern in _COMPILED)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
providers/safety/llm.py DELETED
@@ -1,10 +0,0 @@
1
- from typing import Optional
2
- from core.ports import LLM1Output, LLM2Output
3
-
4
- class LLMRiskSignal:
5
- """
6
- Risk signal that triggers based on the 'risk_flag' explicitly set by LLM1
7
- during its generation of the conversational response.
8
- """
9
- def check(self, message: str, llm1_output: LLM1Output, llm2_output: Optional[LLM2Output] = None) -> bool:
10
- return llm1_output.risk_flag
 
 
 
 
 
 
 
 
 
 
 
providers/speech_to_text_provider.py DELETED
@@ -1,107 +0,0 @@
1
- import io
2
- import re
3
- import tempfile
4
- from typing import Dict, Any
5
-
6
- _EMOTION_MAP = {
7
- "HAPPY": "happy",
8
- "SAD": "sad",
9
- "ANGRY": "angry",
10
- "NEUTRAL": "neutral",
11
- "FEARFUL": "fearful",
12
- "DISGUSTED": "disgusted",
13
- "SURPRISED": "surprised",
14
- }
15
- _KNOWN_EVENTS = {"Speech", "Laughter", "Cry", "Cough", "Sneeze", "Breath", "Noise"}
16
- _TAG_RE = re.compile(r"<\|([^|]+)\|>")
17
-
18
- def _parse_sensevoice_output(raw_text: str) -> tuple[str, str, str | None]:
19
- tags = _TAG_RE.findall(raw_text)
20
- clean = _TAG_RE.sub("", raw_text).strip()
21
-
22
- emotion = "neutral"
23
- event: str | None = None
24
-
25
- for tag in tags:
26
- upper = tag.upper()
27
- if upper in _EMOTION_MAP:
28
- emotion = _EMOTION_MAP[upper]
29
- if tag in _KNOWN_EVENTS:
30
- event = tag
31
-
32
- return clean, emotion, event
33
-
34
- class SenseVoiceSTTProvider:
35
- """
36
- Concrete implementation of the STTProvider protocol using FunASR's SenseVoiceSmall model.
37
- Transcribes audio into text while simultaneously extracting vocal emotional tone
38
- (e.g., happy, sad, angry) and acoustic events (e.g., laughter, crying).
39
- """
40
- def __init__(self):
41
- self._sense_voice = None
42
- self._init_stt()
43
-
44
- def _init_stt(self):
45
- try:
46
- from funasr import AutoModel
47
- from funasr.utils.postprocess_utils import rich_transcription_postprocess
48
-
49
- self._sense_voice = AutoModel(
50
- model="./SenseVoiceSmall",
51
- trust_remote_code=True,
52
- remote_code="./model.py",
53
- vad_model="fsmn-vad",
54
- vad_kwargs={"max_single_segment_time": 30000},
55
- device="cpu",
56
- disable_update=True
57
- )
58
- print("[STARTUP] SenseVoice-Small loaded successfully.")
59
- except Exception as e:
60
- print(f"[WARNING] Failed to load SenseVoice: {e}")
61
- self._sense_voice = None
62
-
63
- def transcribe(self, audio_bytes: bytes) -> Dict[str, Any]:
64
- """
65
- Takes raw webm/ogg bytes, runs them through SenseVoice,
66
- and returns {text, emotion, event}.
67
- """
68
- if not self._sense_voice:
69
- print("[WARNING] STT requested but SenseVoice is not loaded.")
70
- return {"text": "", "emotion": "unknown", "event": None}
71
-
72
- # funasr expects a file path
73
- with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as tf:
74
- tf.write(audio_bytes)
75
- tf.flush()
76
- temp_path = tf.name
77
-
78
- try:
79
- res = self._sense_voice.generate(
80
- input=temp_path,
81
- cache={},
82
- language="auto",
83
- use_itn=True,
84
- batch_size_s=60,
85
- merge_vad=True,
86
- merge_length_s=15,
87
- )
88
- if not res or len(res) == 0:
89
- return {"text": "", "emotion": "neutral", "event": None}
90
-
91
- raw_text = res[0].get("text", "")
92
- clean, emotion, event = _parse_sensevoice_output(raw_text)
93
-
94
- return {
95
- "text": clean,
96
- "emotion": emotion,
97
- "event": event
98
- }
99
- except Exception as e:
100
- print(f"[ERROR] SenseVoice transcription failed: {e}")
101
- return {"text": "", "emotion": "unknown", "event": None}
102
- finally:
103
- import os
104
- try:
105
- os.remove(temp_path)
106
- except OSError:
107
- pass