diff --git a/projekt-matching/components/pm_crm.py b/projekt-matching/components/pm_crm.py new file mode 100644 index 0000000..9fe27aa --- /dev/null +++ b/projekt-matching/components/pm_crm.py @@ -0,0 +1,33 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +from langflow.custom import Component +from langflow.io import DataInput, MessageTextInput, Output, SecretStrInput +from langflow.schema import Data + +from projektmatch import stages +from projektmatch.config import Cfg + + +class PMCrm(Component): + display_name = "PM 5 CRM" + description = "EspoCRM: Team, Firma, Kontakt, Verkaufschance + Verify" + inputs = [ + DataInput(name="ctx", display_name="Context"), + MessageTextInput(name="espo_base", display_name="Espo Base", + value="PM_ESPO_BASE", load_from_db=True, + advanced=True), + SecretStrInput(name="espo_api_key", display_name="Espo API Key", + value="PM_ESPO_API_KEY", load_from_db=True, + advanced=True), + ] + outputs = [Output(name="out", display_name="Context", method="build_out")] + + def build_out(self) -> Data: + cfg = Cfg(espo_base=self.espo_base, espo_api_key=self.espo_api_key) + ctx = stages.run_stage("crm", stages.stage_crm, + dict(self.ctx.data), cfg) + self.status = ctx.get("status", "") + return Data(data=ctx) diff --git a/projekt-matching/components/pm_extract.py b/projekt-matching/components/pm_extract.py new file mode 100644 index 0000000..ccc3cc2 --- /dev/null +++ b/projekt-matching/components/pm_extract.py @@ -0,0 +1,33 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +from langflow.custom import Component +from langflow.io import DataInput, MessageTextInput, Output +from langflow.schema import Data + +from projektmatch import stages +from projektmatch.config import Cfg + + +class PMExtract(Component): + display_name = "PM 2 Extract" + description = "LLM 1: Anforderungen strukturiert extrahieren" + inputs = [ + DataInput(name="ctx", display_name="Context"), + MessageTextInput(name="vllm_base", display_name="vLLM Base", + value="PM_VLLM_BASE", load_from_db=True, + advanced=True), + MessageTextInput(name="vllm_model", display_name="vLLM Model", + value="PM_VLLM_MODEL", load_from_db=True, + advanced=True), + ] + outputs = [Output(name="out", display_name="Context", method="build_out")] + + def build_out(self) -> Data: + cfg = Cfg(vllm_base=self.vllm_base, vllm_model=self.vllm_model) + ctx = stages.run_stage("extract", stages.stage_extract, + dict(self.ctx.data), cfg) + self.status = ctx.get("status", "") + return Data(data=ctx) diff --git a/projekt-matching/components/pm_fetch.py b/projekt-matching/components/pm_fetch.py new file mode 100644 index 0000000..669886a --- /dev/null +++ b/projekt-matching/components/pm_fetch.py @@ -0,0 +1,26 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +import json + +from langflow.custom import Component +from langflow.io import MessageTextInput, Output +from langflow.schema import Data + +from projektmatch import stages + + +class PMFetch(Component): + display_name = "PM 1 Fetch" + description = "Projektseite abrufen (canonical URL -> Seitentext)" + inputs = [MessageTextInput(name="payload", display_name="Payload JSON")] + outputs = [Output(name="out", display_name="Context", method="build_out")] + + def build_out(self) -> Data: + ctx = json.loads(self.payload) + ctx.setdefault("status", "ok") + ctx = stages.run_stage("fetch", stages.stage_fetch, ctx, None) + self.status = ctx.get("status", "") + return Data(data=ctx) diff --git a/projekt-matching/components/pm_ingest.py b/projekt-matching/components/pm_ingest.py new file mode 100644 index 0000000..1936734 --- /dev/null +++ b/projekt-matching/components/pm_ingest.py @@ -0,0 +1,52 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +import json + +from langflow.custom import Component +from langflow.io import DataInput, MessageTextInput, Output, SecretStrInput +from langflow.schema.message import Message + +from projektmatch import ingest +from projektmatch.config import Cfg + + +class PMIngest(Component): + display_name = "PM Ingest" + description = ("IMAP-Postfach abrufen, Projekte splitten, CRM-Dedup, " + "Flow 2 je Projekt, Alert-Mail, Trigger-Mail -> Trash") + inputs = [ + DataInput(name="trigger", display_name="Webhook Trigger"), + SecretStrInput(name="imap_password", display_name="Mail Password", + value="PM_IMAP_PASSWORD", load_from_db=True, + advanced=True), + MessageTextInput(name="espo_base", display_name="Espo Base", + value="PM_ESPO_BASE", load_from_db=True, + advanced=True), + SecretStrInput(name="espo_api_key", display_name="Espo API Key", + value="PM_ESPO_API_KEY", load_from_db=True, + advanced=True), + MessageTextInput(name="alert_to", display_name="Alert To", + value="PM_ALERT_TO", load_from_db=True, + advanced=True), + MessageTextInput(name="flow2_id", display_name="Flow 2 ID", + value="PM_FLOW2_ID", load_from_db=True, + advanced=True), + SecretStrInput(name="langflow_api_key", display_name="Langflow Key", + value="PM_LANGFLOW_API_KEY", load_from_db=True, + advanced=True), + ] + outputs = [Output(name="out", display_name="Summary", method="build_out")] + + def build_out(self) -> Message: + cfg = Cfg(imap_password=self.imap_password, + espo_base=self.espo_base, + espo_api_key=self.espo_api_key, + alert_to=self.alert_to, + flow2_id=self.flow2_id, + langflow_api_key=self.langflow_api_key) + summary = ingest.run_ingest(cfg) + self.status = json.dumps(summary) + return Message(text=json.dumps(summary)) diff --git a/projekt-matching/components/pm_match.py b/projekt-matching/components/pm_match.py new file mode 100644 index 0000000..1fb9726 --- /dev/null +++ b/projekt-matching/components/pm_match.py @@ -0,0 +1,33 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +from langflow.custom import Component +from langflow.io import DataInput, MessageTextInput, Output +from langflow.schema import Data + +from projektmatch import stages +from projektmatch.config import Cfg + + +class PMMatch(Component): + display_name = "PM 3 Match CV" + description = "LLM 2: Anforderungen gegen Lebenslauf bewerten" + inputs = [ + DataInput(name="ctx", display_name="Context"), + MessageTextInput(name="vllm_base", display_name="vLLM Base", + value="PM_VLLM_BASE", load_from_db=True, + advanced=True), + MessageTextInput(name="vllm_model", display_name="vLLM Model", + value="PM_VLLM_MODEL", load_from_db=True, + advanced=True), + ] + outputs = [Output(name="out", display_name="Context", method="build_out")] + + def build_out(self) -> Data: + cfg = Cfg(vllm_base=self.vllm_base, vllm_model=self.vllm_model) + ctx = stages.run_stage("match", stages.stage_match, + dict(self.ctx.data), cfg) + self.status = ctx.get("status", "") + return Data(data=ctx) diff --git a/projekt-matching/components/pm_notify.py b/projekt-matching/components/pm_notify.py new file mode 100644 index 0000000..4023903 --- /dev/null +++ b/projekt-matching/components/pm_notify.py @@ -0,0 +1,34 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +from langflow.custom import Component +from langflow.io import DataInput, MessageTextInput, Output, SecretStrInput +from langflow.schema.message import Message + +from projektmatch import stages +from projektmatch.config import Cfg + + +class PMNotify(Component): + display_name = "PM 6 Notify" + description = "Benachrichtigungs-Mail bei created + Langfuse-Trace" + inputs = [ + DataInput(name="ctx", display_name="Context"), + SecretStrInput(name="imap_password", display_name="Mail Password", + value="PM_IMAP_PASSWORD", load_from_db=True, + advanced=True), + MessageTextInput(name="notify_to", display_name="Notify To", + value="PM_NOTIFY_TO", load_from_db=True, + advanced=True), + ] + outputs = [Output(name="out", display_name="Result", method="build_out")] + + def build_out(self) -> Message: + cfg = Cfg(imap_password=self.imap_password, notify_to=self.notify_to) + # stage_notify is called DIRECTLY (not via run_stage): it must run for + # every terminal status so failed runs still post their Langfuse trace. + ctx = stages.stage_notify(dict(self.ctx.data), cfg) + self.status = ctx.get("status", "") + return Message(text=stages.summary(ctx)) diff --git a/projekt-matching/components/pm_rules.py b/projekt-matching/components/pm_rules.py new file mode 100644 index 0000000..26dc5ec --- /dev/null +++ b/projekt-matching/components/pm_rules.py @@ -0,0 +1,31 @@ +import sys + +if "/app/langflow" not in sys.path: + sys.path.insert(0, "/app/langflow") + +from langflow.custom import Component +from langflow.io import DataInput, MessageTextInput, Output +from langflow.schema import Data + +from projektmatch import stages +from projektmatch.config import Cfg + + +class PMRules(Component): + display_name = "PM 4 Rules+Gate" + description = ("Deterministisch: Misc-Regeln, Match-Berechnung, " + "Beschreibungs-Markdown, Gate > Schwellwert") + inputs = [ + DataInput(name="ctx", display_name="Context"), + MessageTextInput(name="threshold", display_name="Threshold", + value="PM_THRESHOLD", load_from_db=True, + advanced=True), + ] + outputs = [Output(name="out", display_name="Context", method="build_out")] + + def build_out(self) -> Data: + cfg = Cfg(threshold=int(self.threshold)) + ctx = stages.run_stage("rules", stages.stage_rules, + dict(self.ctx.data), cfg) + self.status = f"{ctx.get('decision')} ({ctx.get('mustMatch')} %)" + return Data(data=ctx)