232 lines
13 KiB
Markdown
232 lines
13 KiB
Markdown
---
|
||
name: projekt-anlegen
|
||
description: >-
|
||
Bewertet Freiberufler-Projektausschreibungen gegen den Lebenslauf von Dr.-Ing. Thomas Langer
|
||
und trägt das Ergebnis als Verkaufschance (Opportunity) ins EspoCRM ein: Anforderungen
|
||
extrahieren, in Must/Nice/Misc einordnen, Abdeckung mit ✅/❌/❔ bewerten, Must-have-Match und
|
||
Nice-to-have-Match berechnen, nach Review im Chat ins CRM schreiben. Verwende diesen Skill
|
||
IMMER, wenn der Nutzer ein Projektangebot, eine Projektausschreibung oder ein Stellenangebot
|
||
für Freiberufler bewerten oder ins CRM eintragen will (z. B. von freelancermap, GULP/Randstad,
|
||
Hays, SThree, etwa „Bewerte das Projekt", „Projekt-Match", „Passt dieses Projekt zu mir?",
|
||
„Anforderungen abgleichen", „Projekt anlegen", „Projekt ins CRM eintragen").
|
||
---
|
||
|
||
# Projekt anlegen: Projektangebote bewerten und ins EspoCRM eintragen
|
||
|
||
Pipeline: Ausschreibung beschaffen → Anforderungen + Käufer/Firma/Kontakt extrahieren → einordnen → gegen Lebenslauf bewerten → Rahmenbedingungen gegen Vorgaben bewerten → Matches berechnen → **Review im Chat (Pflicht)** → Firma → Kontakt → Verkaufschance ins EspoCRM schreiben (per Shell) → verifizieren.
|
||
|
||
## Voraussetzungen und feste Pfade
|
||
|
||
| Zweck | Pfad |
|
||
|---|---|
|
||
| Lebenslauf (Markdown) | `vorgaben/Lebenslauf_Dr-Ing_Thomas_Langer.md` |
|
||
| Rahmenbedingungen (Misc-Bewertung) | `vorgaben/rahmenbedingungen.md` |
|
||
| EspoCRM-Zugangsdaten | `.secrets/espocrm-api.md` |
|
||
|
||
Alle drei mit dem Read-Tool lesen. Ohne Lebenslauf keine fachliche Bewertung, ohne Rahmenbedingungen keine automatische Misc-Bewertung, ohne Zugangsdaten kein CRM-Eintrag.
|
||
|
||
Die Zugangsdaten-Datei enthält Base-URL, Auth-Header-Schema, API-Key sowie verifizierte Entitäts- und Feldnamen. **Der API-Key darf niemals im Chat angezeigt, in Dateien geschrieben oder in Commits/Doku übernommen werden** — er wird nur zur Laufzeit in eine Shell-Variable gelesen und direkt in `curl`-Aufrufe eingesetzt.
|
||
|
||
## Schritt 1: Ausschreibung beschaffen und Eckdaten extrahieren
|
||
|
||
Die Projektausschreibung kommt auf zwei Wegen:
|
||
- **URL:** Nennt der Nutzer eine URL, hole den Seitentext mit dem **WebFetch-Tool**.
|
||
- **Text:** Fügt der Nutzer den Ausschreibungstext direkt in den Chat ein, nutze diesen.
|
||
|
||
Bei Login-geschützten Portalen ist das Einfügen von Text nötig. Ist unklar, welche Quelle gilt, nachfragen.
|
||
|
||
Daraus entnehmen:
|
||
- **Projektname**: der Titel der Ausschreibung, ohne Portal-Zusatz (z. B. ohne „auf www.freelancermap.de").
|
||
- **Projekt-URL**: die tatsächliche URL der Seite, falls vorhanden.
|
||
- **Beschreibung und Aufgaben**: nur Kontext für die Bewertung, keine Tabellenzeilen.
|
||
- **Anforderungen**: jede Anforderung einzeln, im Originalwortlaut (behutsam kürzen ist erlaubt, Bedeutung nie verändern). Rahmenbedingungen (Start, Einsatzort/Remote-Anteil, Auslastung, Laufzeit) zählen als Anforderungen der Kategorie Misc.
|
||
- **Käufer-Typ**: Agentur (Wiederverkäufer) ODER Endkunde (Direktauftrag).
|
||
- Agentur-Signale: bekannte Personaldienstleister/Vermittler (Hays, GULP/Randstad, SThree/Computer Futures, Aristo …), Formulierungen wie „im Auftrag unseres Kunden", „für unseren Kunden", Vermittler-Kontext des Portals.
|
||
- Nicht eindeutig bestimmbar → im Review nachfragen.
|
||
- **Firmenname**: Name der Agentur bzw. des Endkunden.
|
||
- **Ansprechperson**: vollständiger Name, falls genannt.
|
||
|
||
## Schritt 2: Einordnung (Kat.)
|
||
|
||
Jede Anforderung bekommt genau eine Kategorie:
|
||
|
||
- **Must** — zwingend. Signale: eigener Abschnitt „Must-haves"/„Anforderungen", Wörter wie „zwingend", „erforderlich", „vorausgesetzt", „sehr gute Kenntnisse".
|
||
- **Nice** — optional. Signale: Abschnitt „Nice-to-haves", Wörter wie „von Vorteil", „wünschenswert", „idealerweise", „plus".
|
||
- **Misc** — Rahmenbedingungen (Start, Ort, Auslastung, Laufzeit) und alles, was weder Muss noch Wunsch-Qualifikation ist.
|
||
|
||
Explizite Abschnittsüberschriften der Ausschreibung haben Vorrang vor Signalwörtern. Steht „idealerweise" innerhalb einer Must-Anforderung (z. B. „… und idealerweise Kubernetes"), bleibt die Zeile Must — der idealerweise-Teil ist dann für die Bewertung nicht ausschlaggebend.
|
||
|
||
## Schritt 3: Bewertung gegen den Lebenslauf
|
||
|
||
Lebenslauf vollständig lesen und jede Anforderung nüchtern bewerten:
|
||
|
||
- **✅** nur bei klarer Evidenz im Lebenslauf.
|
||
- **❌** wenn der Lebenslauf nichts Belastbares hergibt. Streng bleiben — eine geschönte Bewertung macht die Match-Zahlen wertlos. Kalibrierung aus der Praxis: Proof-of-Concept-Erfahrung deckt „produktiven Betrieb" nicht ab; „mehrjährig" wörtlich nehmen; ein ALM-Produktname (z. B. „Azure DevOps Server") belegt keine Cloud-Plattform-Erfahrung.
|
||
- **❔** nur bei echter Teilevidenz, wenn die Entscheidung von Wissen abhängt, das nur Thomas hat — solche Fälle klärt das Review.
|
||
- „wie z. B."-Aufzählungen: gleichwertige Alternativen zählen als Abdeckung (Beispiel: Ollama/llama.cpp/Transformers decken „LLM-Inference-Stacks wie z. B. vLLM, TGI, Triton" ab).
|
||
- Rahmenbedingungen (Misc) werden NICHT hier, sondern in Schritt 3b automatisch gegen `vorgaben/rahmenbedingungen.md` bewertet.
|
||
|
||
## Schritt 3b: Rahmenbedingungen automatisch bewerten (Misc)
|
||
|
||
`vorgaben/rahmenbedingungen.md` lesen und jede Misc-Anforderung danach bewerten:
|
||
|
||
- **Verfügbar ab / Projektstart:** Start ≤ heute + 8 Wochen (56 Tage) → ✅; > 8 Wochen → ❌; nicht genannt → ❔. („heute" = aktuelles Datum.)
|
||
- **Auslastung:** 75–100 % → ✅; < 75 % → ❔; nicht genannt → ❔.
|
||
- **Laufzeit:** jede → ✅.
|
||
- **Einsatzort/Remote:** 100 % Remote → ✅. Bei < 100 % Remote (Hybrid/Onsite) Distanz des Onsite-Orts zu Sauerlach bestimmen (siehe unten): ≤ 50 km → ✅; 50–60 km → ❔; > 60 km → ❌. Einsatzort/Remote-Anteil unklar oder nicht genannt → ❌.
|
||
|
||
**Distanzbestimmung (Geokodierung, Pflicht bei Onsite/Hybrid):** Onsite-Ort per Nominatim auflösen und Luftlinie zu Sauerlach (47,9721 N / 11,6528 O) per Haversine rechnen. Bei Geokodierungs-Fehler/Mehrdeutigkeit (Ausgabe `UNBEKANNT`) → ❔. Nominatim-Policy: max. 1 Request/s.
|
||
|
||
```bash
|
||
python3 - "<ONSITE_ORT>" <<'PY'
|
||
import sys, json, urllib.request, urllib.parse
|
||
from math import radians, sin, cos, asin, sqrt
|
||
ort = sys.argv[1]
|
||
UA = 'bewerb-projekt-anlegen/1.0 (Thomas.Langer@destengs.com)'
|
||
def geo(q):
|
||
url = 'https://nominatim.openstreetmap.org/search?' + urllib.parse.urlencode(
|
||
{'q': q, 'format': 'json', 'countrycodes': 'de', 'limit': 1})
|
||
d = json.load(urllib.request.urlopen(
|
||
urllib.request.Request(url, headers={'User-Agent': UA}), timeout=20))
|
||
return (float(d[0]['lat']), float(d[0]['lon'])) if d else None
|
||
S = (47.9721357, 11.6528398)
|
||
g = geo(ort)
|
||
if not g:
|
||
print('UNBEKANNT')
|
||
else:
|
||
dlat = radians(g[0]-S[0]); dlon = radians(g[1]-S[1])
|
||
h = sin(dlat/2)**2 + cos(radians(S[0]))*cos(radians(g[0]))*sin(dlon/2)**2
|
||
print(f'{2*6371*asin(sqrt(h)):.1f} km')
|
||
PY
|
||
```
|
||
|
||
## Schritt 4: Match-Berechnung
|
||
|
||
- **Must-have-Match**: Mittelwert über alle Must-Zeilen mit ✅ = 100 %, ❔ = 25 %, ❌ = 0 %.
|
||
- **Nice-to-have-Match**: dasselbe über alle Nice-Zeilen.
|
||
- Misc-Zeilen gehen in keinen Match ein.
|
||
- Auf ganze Prozent runden. Ist eine Kategorie leer, „–" statt einer Zahl schreiben.
|
||
|
||
## Schritt 5: Ausgabeformat (exakt einhalten)
|
||
|
||
Der Inhalt des CRM-Feldes „Beschreibung" — genau dieses Format, Spaltenüberschriften wörtlich (`Kat.` und `❔` halten die Spalten schmal):
|
||
|
||
```markdown
|
||
Must-have-Match: 50 % · Nice-to-have-Match: 33 %
|
||
|
||
| Nr. | Kat. | ❔ | Anforderung |
|
||
|---|---|---|---|
|
||
| 1 | Must | ❌ | Mehrjährige praktische Erfahrung im Aufbau und Betrieb von AI-/LLM-Plattformen |
|
||
| 2 | Must | ✅ | Erfahrung mit RAG-Systemen, Vektordatenbanken und Anbindung relevanter Datenquellen |
|
||
| 3 | Nice | ✅ | Kenntnisse in LLM-Inference-Stacks (z. B. vLLM, TGI, Triton, Ray) |
|
||
| 4 | Misc | ✅ | Hybrid: ca. 40 % onsite München, 60 % remote |
|
||
```
|
||
|
||
- `Nr.` fortlaufend ab 1, Reihenfolge: erst alle Must-, dann Nice-, dann Misc-Zeilen (innerhalb der Kategorie in Reihenfolge der Ausschreibung).
|
||
- `Kat.` ∈ {Must, Nice, Misc}; Bewertungsspalte ∈ {✅, ❌, ❔}.
|
||
- Beide Match-Zahlen in einer Zeile, getrennt durch ` · `.
|
||
|
||
## Schritt 6: Review im Chat — immer vor jedem CRM-Schreibvorgang
|
||
|
||
Niemals ohne explizite Freigabe ins CRM schreiben. Im Chat zeigen:
|
||
|
||
1. Vorgesehener **Name** der Opportunity (= Projektname) und **Projektlink**.
|
||
2. **Käufer-Typ** (Agentur/Direktauftrag) mit Kurzbegründung.
|
||
3. **Firma:** Name und `type` (Reseller bei Agentur, Customer bei Direktauftrag); „neu anlegen" oder „bestehende nutzen: <Name/ID>". Bei ähnlichen, nicht identischen Treffern die Kandidatenliste zeigen und entscheiden lassen.
|
||
4. **Kontakt:** Name; „neu anlegen" oder „bestehend nutzen: <Name/ID>".
|
||
5. **Verknüpfungs-Zuordnung** der Verkaufschance (account vs. cAccount1, Kontakt).
|
||
6. Den wörtlichen Beschreibungs-Markdown in einem Codeblock.
|
||
7. Kurze Begründungen zu allen ❌- und ❔-Bewertungen (Must/Nice/Misc) außerhalb der Tabelle.
|
||
|
||
Korrekturen kommen als „Nr. X → ✅/❌/❔" oder als Korrektur zu Firma/Kontakt/Typ. Danach betroffene Matches neu berechnen und geänderte Werte nennen. Erst nach Freigabe weiter zu Schritt 7.
|
||
|
||
## Schritt 7: CRM-Eintrag per Shell — Reihenfolge Firma → Kontakt → Verkaufschance
|
||
|
||
Diese Umgebung erreicht die CRM-Domain direkt; der Zugriff läuft per `curl`. Key/Base sicher laden (Werte nicht ausgeben):
|
||
|
||
```bash
|
||
KEY=$(grep -oE '[0-9a-f]{32}' .secrets/espocrm-api.md | head -1)
|
||
BASE=$(grep -oE 'https://[^ `]+/api/v1' .secrets/espocrm-api.md | head -1)
|
||
```
|
||
|
||
### 7.1 Firma (Account)
|
||
Dedup-Suche mit einem Kern-Token des Namens (z. B. „Aristo"):
|
||
|
||
```bash
|
||
curl -s -G "$BASE/Account" -H "X-Api-Key: $KEY" \
|
||
--data-urlencode 'where[0][type]=contains' \
|
||
--data-urlencode 'where[0][attribute]=name' \
|
||
--data-urlencode 'where[0][value]=<KERN>' \
|
||
--data-urlencode 'select=name,type' --data-urlencode 'maxSize=50'
|
||
```
|
||
|
||
Treffer gemäß Review-Entscheidung behandeln. Bestehende Firma → deren `id` als ACCOUNT_ID merken. Sonst neu anlegen — Payload mit dem **Write-Tool** nach `/tmp/account.json` (`type`: `Reseller` bei Agentur, `Customer` bei Direktauftrag):
|
||
|
||
```json
|
||
{"name": "<FIRMENNAME>", "type": "<Reseller|Customer>"}
|
||
```
|
||
|
||
```bash
|
||
curl -s -i -X POST "$BASE/Account" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/account.json
|
||
```
|
||
|
||
`id` aus der Antwort als ACCOUNT_ID merken.
|
||
|
||
### 7.2 Kontakt (Contact)
|
||
Dedup-Suche per Nachname:
|
||
|
||
```bash
|
||
curl -s -G "$BASE/Contact" -H "X-Api-Key: $KEY" \
|
||
--data-urlencode 'where[0][type]=contains' \
|
||
--data-urlencode 'where[0][attribute]=name' \
|
||
--data-urlencode 'where[0][value]=<NACHNAME>' \
|
||
--data-urlencode 'select=name,accountName' --data-urlencode 'maxSize=50'
|
||
```
|
||
|
||
Bestehenden Kontakt → dessen `id` als CONTACT_ID merken. Sonst neu anlegen — Payload nach `/tmp/contact.json`; die **Verknüpfung zur Firma über `accountId` ist beim Neu-Anlegen zwingend**:
|
||
|
||
```json
|
||
{"firstName": "<VORNAME>", "lastName": "<NACHNAME>", "accountId": "<ACCOUNT_ID>"}
|
||
```
|
||
|
||
```bash
|
||
curl -s -i -X POST "$BASE/Contact" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/contact.json
|
||
```
|
||
|
||
`id` aus der Antwort als CONTACT_ID merken. Namensaufteilung: letztes Token = `lastName`, der Rest = `firstName`; ungewöhnliche Fälle im Review klären.
|
||
|
||
### 7.3 Verkaufschance (Opportunity)
|
||
Duplikat-Prüfung des Namens (bei Treffer Suffix „ (2)", „ (3)", … anhängen):
|
||
|
||
```bash
|
||
curl -s -G "$BASE/Opportunity" -H "X-Api-Key: $KEY" \
|
||
--data-urlencode 'where[0][type]=startsWith' \
|
||
--data-urlencode 'where[0][attribute]=name' \
|
||
--data-urlencode 'where[0][value]=<PROJEKTNAME>' \
|
||
--data-urlencode 'select=name' --data-urlencode 'maxSize=100'
|
||
```
|
||
|
||
Payload nach `/tmp/opp.json`. Verknüpfungen je Käufer-Typ:
|
||
- **Agentur:** `cAccount1Id` = ACCOUNT_ID („Über Agentur"); **kein** `accountId`.
|
||
- **Direktauftrag:** `accountId` = ACCOUNT_ID; **kein** `cAccount1Id`.
|
||
|
||
```json
|
||
{"name": "<EINDEUTIGER_NAME>", "description": "<BESCHREIBUNG_MARKDOWN>", "cProjektlink": "<PROJEKT_URL>", "contactsIds": ["<CONTACT_ID>"], "cAccount1Id": "<ACCOUNT_ID — nur bei Agentur, sonst Feld weglassen und stattdessen accountId setzen>"}
|
||
```
|
||
|
||
```bash
|
||
curl -s -i -X POST "$BASE/Opportunity" -H "X-Api-Key: $KEY" -H 'Content-Type: application/json' --data @/tmp/opp.json
|
||
```
|
||
|
||
Minimaler Payload genügt (Stage/Wahrscheinlichkeit setzt das CRM). Bei HTTP 400 steht die Ursache im `X-Status-Reason`-Header. Nachträgliche Korrekturen: `PUT "$BASE/Opportunity/<id>"`. Danach `/tmp/account.json /tmp/contact.json /tmp/opp.json` löschen.
|
||
|
||
## Schritt 8: Verifizieren und berichten
|
||
|
||
Nach dem POST den Datensatz gegenlesen:
|
||
|
||
```bash
|
||
curl -s -G "$BASE/Opportunity/<id>" -H "X-Api-Key: $KEY"
|
||
```
|
||
|
||
Prüfen: stimmen Name und Projektlink, hat die Beschreibung die Match-Zeile und alle Tabellenzeilen, sind ✅/❌/❔ intakt; sind die Verknüpfungen korrekt — Agentur: `cAccount1Name` gesetzt und `accountName` leer; Direktauftrag: `accountName` gesetzt; `contactsIds`/`contactId` enthält den Kontakt. Dann im Chat melden: gewählter Name (inkl. evtl. Duplikat-Suffix), beide Match-Zahlen, Firma und Kontakt (jeweils neu/bestehend) und der Direktlink `https://crm.creature-go.com/#Opportunity/view/<id>`.
|