dasein-core 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dasein/api.py CHANGED
@@ -2714,8 +2714,9 @@ Follow these rules when planning your actions."""
2714
2714
  trace = self._extract_trace_from_langgraph_result(result, extracted_query)
2715
2715
 
2716
2716
  if not trace:
2717
- print(f"[DASEIN] No trace available for synthesis")
2718
- return None
2717
+ # Do not bail out – continue with empty trace so KPIs can still be recorded by the service
2718
+ print(f"[DASEIN] No trace available for synthesis - continuing with empty trace for KPIs")
2719
+ trace = []
2719
2720
 
2720
2721
  print(f"[DASEIN] Sending trace with {len(trace)} steps to post-run service")
2721
2722
 
@@ -2761,11 +2762,68 @@ Follow these rules when planning your actions."""
2761
2762
  cleaned_trace = clean_for_json(trace)
2762
2763
 
2763
2764
  # 3) Call post-run service for synthesis
2765
+ # For retry > 1, wait for synthesis on all runs except the last one
2766
+ wait_for_synthesis = False
2767
+ if total_steps and step_number and step_number < total_steps:
2768
+ wait_for_synthesis = True
2769
+ print(f"[DASEIN] Will WAIT for rule synthesis (step {step_number}/{total_steps})")
2770
+
2764
2771
  if self._post_run == "kpi_only":
2765
2772
  print(f"[DASEIN] Calling post-run service (KPI-only mode, no rule synthesis)")
2766
2773
  else:
2767
- print(f"[DASEIN] Calling post-run service for rule synthesis")
2774
+ mode_str = "BLOCKING" if wait_for_synthesis else "ASYNC"
2775
+ print(f"[DASEIN] Calling post-run service for rule synthesis ({mode_str} mode)")
2768
2776
 
2777
+ # Compute agent fingerprint for post-run (mirror pre-run minimal fingerprint)
2778
+ def _minimal_agent_fingerprint(agent) -> str:
2779
+ try:
2780
+ agent_cls = getattr(agent, '__class__', None)
2781
+ agent_name = getattr(agent_cls, '__name__', '') if agent_cls else ''
2782
+ module = getattr(agent, '__module__', '') or ''
2783
+ framework = module.split('.')[0] if module else ''
2784
+ model_id = ''
2785
+ llm = getattr(agent, 'llm', None)
2786
+ if llm is not None:
2787
+ model_id = (
2788
+ getattr(llm, 'model', None)
2789
+ or getattr(llm, 'model_name', None)
2790
+ or getattr(llm, 'model_id', None)
2791
+ or getattr(llm, 'model_tag', None)
2792
+ or ''
2793
+ )
2794
+ tool_names = []
2795
+ tools_attr = getattr(agent, 'tools', None)
2796
+ if tools_attr:
2797
+ try:
2798
+ for t in tools_attr:
2799
+ name = getattr(t, 'name', None) or getattr(t, '__name__', None) or getattr(t.__class__, '__name__', '')
2800
+ if name:
2801
+ tool_names.append(str(name))
2802
+ except Exception:
2803
+ pass
2804
+ elif getattr(agent, 'toolkit', None):
2805
+ tk = getattr(agent, 'toolkit')
2806
+ tk_tools = getattr(tk, 'tools', None) or getattr(tk, 'get_tools', None)
2807
+ try:
2808
+ iterable = tk_tools() if callable(tk_tools) else tk_tools
2809
+ for t in (iterable or []):
2810
+ name = getattr(t, 'name', None) or getattr(t, '__name__', None) or getattr(t.__class__, '__name__', '')
2811
+ if name:
2812
+ tool_names.append(str(name))
2813
+ except Exception:
2814
+ pass
2815
+ norm = lambda s: str(s).strip().lower().replace(' ', '_') if s is not None else ''
2816
+ agent_name = norm(agent_name)
2817
+ framework = norm(framework)
2818
+ model_id = norm(model_id)
2819
+ tool_names = [norm(n) for n in tool_names if n]
2820
+ tools_joined = ','.join(sorted(set(tool_names)))
2821
+ return f"[[FINGERPRINT]] agent={agent_name} | framework={framework} | model={model_id} | tools={tools_joined}"
2822
+ except Exception:
2823
+ return getattr(agent, 'agent_id', None) or f"agent_{id(agent)}"
2824
+
2825
+ agent_fingerprint = _minimal_agent_fingerprint(self._agent)
2826
+
2769
2827
  response = self._service_adapter.synthesize_rules(
2770
2828
  run_id=None, # Will use stored run_id from pre-run phase
2771
2829
  trace=cleaned_trace,
@@ -2776,9 +2834,10 @@ Follow these rules when planning your actions."""
2776
2834
  max_rules=self._top_k, # Configurable via top_k parameter
2777
2835
  performance_tracking_id=self._performance_tracking_id, # For rule isolation
2778
2836
  skip_synthesis=skip_synthesis, # Skip expensive synthesis when not needed
2779
- agent_fingerprint=getattr(self._agent, 'agent_id', None) or f"agent_{id(self._agent)}",
2837
+ agent_fingerprint=agent_fingerprint, # Reuse fingerprint from pre-run (line 2613)
2780
2838
  step_id=self._current_step_id, # Pass step_id for parallel execution tracking
2781
- post_run_mode=self._post_run # Pass post_run mode ("full" or "kpi_only")
2839
+ post_run_mode=self._post_run, # Pass post_run mode ("full" or "kpi_only")
2840
+ wait_for_synthesis=wait_for_synthesis # Wait for synthesis on retry runs (except last)
2782
2841
  )
2783
2842
 
2784
2843
  # response is a dict from ServiceAdapter; handle accordingly
dasein/capture.py CHANGED
@@ -405,10 +405,26 @@ class DaseinCallbackHandler(BaseCallbackHandler):
405
405
 
406
406
  # 🚨 OPTIMIZED: For LangGraph, check if kwargs contains 'invocation_params' with messages
407
407
  # Extract the most recent message instead of full history
408
+ # Use from_end=True to capture the END of system prompts (where user's actual query is)
408
409
  if 'invocation_params' in kwargs and 'messages' in kwargs['invocation_params']:
409
410
  args_excerpt = self._extract_recent_message({'messages': kwargs['invocation_params']['messages']})
410
411
  else:
411
- args_excerpt = self._excerpt(" | ".join(modified_prompts))
412
+ args_excerpt = self._excerpt(" | ".join(modified_prompts), from_end=True)
413
+
414
+ # GNN-related fields
415
+ step_index = len(self._trace)
416
+
417
+ # Track which rules triggered at this step (llm_start rules)
418
+ rule_triggered_here = []
419
+ if hasattr(self, '_selected_rules') and self._selected_rules:
420
+ for rule_meta in self._selected_rules:
421
+ if isinstance(rule_meta, tuple) and len(rule_meta) == 2:
422
+ rule_obj, _metadata = rule_meta
423
+ else:
424
+ rule_obj = rule_meta
425
+ target_step_type = getattr(rule_obj, 'target_step_type', '')
426
+ if target_step_type in ['llm_start', 'chain_start']:
427
+ rule_triggered_here.append(getattr(rule_obj, 'id', 'unknown'))
412
428
 
413
429
  step = {
414
430
  "step_type": "llm_start",
@@ -419,6 +435,9 @@ class DaseinCallbackHandler(BaseCallbackHandler):
419
435
  "run_id": None,
420
436
  "parent_run_id": None,
421
437
  "node": self._current_chain_node, # LangGraph node name (if available)
438
+ # GNN step-level fields
439
+ "step_index": step_index,
440
+ "rule_triggered_here": rule_triggered_here,
422
441
  }
423
442
  self._trace.append(step)
424
443
  # self._vprint(f"[DASEIN][CALLBACK] Captured llm_start: {len(_TRACE)} total steps") # Commented out - too noisy
@@ -583,6 +602,15 @@ class DaseinCallbackHandler(BaseCallbackHandler):
583
602
  import traceback
584
603
  traceback.print_exc()
585
604
 
605
+ # GNN-related fields: compute tokens_delta
606
+ step_index = len(self._trace)
607
+ tokens_delta = 0
608
+ # Find previous step with tokens_output to compute delta
609
+ for prev_step in reversed(self._trace):
610
+ if 'tokens_output' in prev_step and prev_step['tokens_output'] > 0:
611
+ tokens_delta = output_tokens - prev_step['tokens_output']
612
+ break
613
+
586
614
  step = {
587
615
  "step_type": "llm_end",
588
616
  "tool_name": "",
@@ -594,6 +622,9 @@ class DaseinCallbackHandler(BaseCallbackHandler):
594
622
  "tokens_input": input_tokens,
595
623
  "tokens_output": output_tokens,
596
624
  "node": self._current_chain_node, # LangGraph node name (if available)
625
+ # GNN step-level fields
626
+ "step_index": step_index,
627
+ "tokens_delta": tokens_delta,
597
628
  }
598
629
  self._trace.append(step)
599
630
 
@@ -673,6 +704,21 @@ class DaseinCallbackHandler(BaseCallbackHandler):
673
704
 
674
705
  args_excerpt = self._excerpt(modified_input)
675
706
 
707
+ # GNN-related fields: capture step-level metrics
708
+ step_index = len(self._trace)
709
+ tool_input_chars = len(str(input_str))
710
+
711
+ # Track which rules triggered at this step
712
+ rule_triggered_here = []
713
+ if hasattr(self, '_selected_rules') and self._selected_rules:
714
+ for rule_meta in self._selected_rules:
715
+ if isinstance(rule_meta, tuple) and len(rule_meta) == 2:
716
+ rule_obj, _metadata = rule_meta
717
+ else:
718
+ rule_obj = rule_meta
719
+ if getattr(rule_obj, 'target_step_type', '') == "tool_start":
720
+ rule_triggered_here.append(getattr(rule_obj, 'id', 'unknown'))
721
+
676
722
  step = {
677
723
  "step_type": "tool_start",
678
724
  "tool_name": tool_name,
@@ -682,6 +728,10 @@ class DaseinCallbackHandler(BaseCallbackHandler):
682
728
  "run_id": run_id,
683
729
  "parent_run_id": parent_run_id,
684
730
  "node": self._current_chain_node, # LangGraph node name (if available)
731
+ # GNN step-level fields
732
+ "step_index": step_index,
733
+ "tool_input_chars": tool_input_chars,
734
+ "rule_triggered_here": rule_triggered_here,
685
735
  }
686
736
  self._trace.append(step)
687
737
 
@@ -707,6 +757,24 @@ class DaseinCallbackHandler(BaseCallbackHandler):
707
757
  # self._vprint(f"[DASEIN][CALLBACK] Output length: {len(output_str)} chars") # Commented out - too noisy
708
758
  # self._vprint(f"[DASEIN][CALLBACK] Outcome length: {len(outcome)} chars") # Commented out - too noisy
709
759
 
760
+ # GNN-related fields: capture tool output metrics
761
+ step_index = len(self._trace)
762
+ tool_output_chars = len(output_str)
763
+
764
+ # Estimate tool_output_items (heuristic: count lines, or rows if SQL-like)
765
+ tool_output_items = 0
766
+ try:
767
+ # Try to count lines as a proxy for items
768
+ if output_str:
769
+ tool_output_items = output_str.count('\n') + 1
770
+ except:
771
+ tool_output_items = 0
772
+
773
+ # Extract available selectors from DOM-like output (web browse agents)
774
+ available_selectors = None
775
+ if tool_name in ['extract_text', 'get_elements', 'extract_hyperlinks', 'extract_content']:
776
+ available_selectors = self._extract_semantic_selectors(output_str)
777
+
710
778
  step = {
711
779
  "step_type": "tool_end",
712
780
  "tool_name": tool_name,
@@ -716,7 +784,15 @@ class DaseinCallbackHandler(BaseCallbackHandler):
716
784
  "run_id": run_id,
717
785
  "parent_run_id": parent_run_id,
718
786
  "node": self._current_chain_node, # LangGraph node name (if available)
787
+ # GNN step-level fields
788
+ "step_index": step_index,
789
+ "tool_output_chars": tool_output_chars,
790
+ "tool_output_items": tool_output_items,
719
791
  }
792
+
793
+ # Add available_selectors only if found (keep trace light)
794
+ if available_selectors:
795
+ step["available_selectors"] = available_selectors
720
796
  self._trace.append(step)
721
797
 
722
798
  # Clean up the stored tool name
@@ -880,12 +956,83 @@ class DaseinCallbackHandler(BaseCallbackHandler):
880
956
  # On any error, fall back to original behavior
881
957
  return self._excerpt(str(inputs))
882
958
 
883
- def _excerpt(self, obj: Any, max_len: int = 250) -> str:
884
- """Truncate text to max_length with ellipsis."""
959
+ def _excerpt(self, obj: Any, max_len: int = 250, from_end: bool = False) -> str:
960
+ """
961
+ Truncate text to max_length with ellipsis.
962
+
963
+ Args:
964
+ obj: Object to convert to string and truncate
965
+ max_len: Maximum length of excerpt
966
+ from_end: If True, take LAST max_len chars (better for system prompts).
967
+ If False, take FIRST max_len chars (better for tool args).
968
+ """
885
969
  text = str(obj)
886
970
  if len(text) <= max_len:
887
971
  return text
888
- return text[:max_len-3] + "..."
972
+
973
+ if from_end:
974
+ # Take last X chars - better for system prompts where the end contains user's actual query
975
+ return "..." + text[-(max_len-3):]
976
+ else:
977
+ # Take first X chars - better for tool inputs
978
+ return text[:max_len-3] + "..."
979
+
980
+ def _extract_semantic_selectors(self, html_text: str) -> List[Dict[str, int]]:
981
+ """
982
+ Extract semantic HTML tags from output for grounding web browse rules.
983
+ Only extracts semantic tags (nav, header, h1, etc.) to keep trace lightweight.
984
+
985
+ Args:
986
+ html_text: Output text that may contain HTML
987
+
988
+ Returns:
989
+ List of {"tag": str, "count": int} sorted by count descending, or None if no HTML
990
+ """
991
+ import re
992
+
993
+ # Quick check: does this look like HTML?
994
+ if '<' not in html_text or '>' not in html_text:
995
+ return None
996
+
997
+ # Semantic tags we care about (prioritized for web browse agents)
998
+ semantic_tags = [
999
+ # Navigation/Structure (highest priority)
1000
+ 'nav', 'header', 'footer', 'main', 'article', 'section', 'aside',
1001
+
1002
+ # Headers (critical for "find headers" queries!)
1003
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
1004
+
1005
+ # Interactive
1006
+ 'a', 'button', 'form', 'input', 'textarea', 'select', 'label',
1007
+
1008
+ # Lists (often used for navigation)
1009
+ 'ul', 'ol', 'li',
1010
+
1011
+ # Tables (data extraction)
1012
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
1013
+
1014
+ # Media
1015
+ 'img', 'video', 'audio'
1016
+ ]
1017
+
1018
+ # Count occurrences of each semantic tag
1019
+ found_tags = {}
1020
+ for tag in semantic_tags:
1021
+ # Pattern: <tag ...> or <tag> (opening tags only)
1022
+ pattern = f'<{tag}[\\s>]'
1023
+ matches = re.findall(pattern, html_text, re.IGNORECASE)
1024
+ if matches:
1025
+ found_tags[tag] = len(matches)
1026
+
1027
+ # Return None if no semantic tags found
1028
+ if not found_tags:
1029
+ return None
1030
+
1031
+ # Convert to list format, sorted by count descending
1032
+ # Limit to top 15 to keep trace light
1033
+ result = [{"tag": tag, "count": count}
1034
+ for tag, count in sorted(found_tags.items(), key=lambda x: -x[1])]
1035
+ return result[:15] # Top 15 most common tags
889
1036
 
890
1037
  def set_selected_rules(self, rules: List[Dict[str, Any]]):
891
1038
  """Set the rules selected for this run.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dasein-core
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Universal memory for agentic AI. Attach a brain to any LangChain/LangGraph agent in a single line.
5
5
  Author-email: Dasein Team <support@dasein.ai>
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  dasein/__init__.py,sha256=RY0lhaaWB6yJ_5YMRmaHDvQ0eFbc0BGbYNe5OVyxzYE,2316
2
2
  dasein/advice_format.py,sha256=5-h4J24L_B2Y9dlmyDuIYtmPCWOGAYoinBEXqpcNg2s,5386
3
- dasein/api.py,sha256=yVR5CoHIIHV9DedRFEpw8pY0EkLovSqJH1-tvR7uZ_Q,172105
4
- dasein/capture.py,sha256=pTneyQPhJCBUPyIpOCsRtU-W7Wprw5IXdMkeN_bbh0E,74330
3
+ dasein/api.py,sha256=yYymqTKwtUuQ1dnHFHt9L7OuEXQddVjyxBMa8Py4MmE,175714
4
+ dasein/capture.py,sha256=IhvG-seZW5dyX-6COR4w9Xhe1oJ-QltCvg158qjhEb4,80743
5
5
  dasein/config.py,sha256=lXO8JG4RXbodn3gT5yEnuB0VRwWdrRVwhX3Rm06IZmU,1957
6
6
  dasein/events.py,sha256=mG-lnOvQoZUhXbrPSjrG4RME6ywUcbSZ04PscoJ15GI,12896
7
7
  dasein/extractors.py,sha256=fUFBVH9u2x4cJaM-8Zw4qiIpBF2LvjcdYkMvoXQUpL8,3986
@@ -14,8 +14,8 @@ dasein/services/post_run_client.py,sha256=bzWKiu-FGo36r6J-QBv6zjLpjI7mMjKmjhvfid
14
14
  dasein/services/pre_run_client.py,sha256=tXmz_PQaSfq0xwypiWUAqNkXOmREZ6EwXLC4OM89J-A,4317
15
15
  dasein/services/service_adapter.py,sha256=nVvPPioYKoxYmR33w1ssYx422UPBeTsq8yIKZum8YHg,7128
16
16
  dasein/services/service_config.py,sha256=8_4tpV4mZvfaOc5_yyHbOyL4rYsPHzkLTEY1rtYgLs8,1629
17
- dasein_core-0.2.1.dist-info/licenses/LICENSE,sha256=7FHjIFEKl_3hSc3tGUVEWmufC_3oi8rh_2zVuL7jMKs,1091
18
- dasein_core-0.2.1.dist-info/METADATA,sha256=MdnWuESn6u1_HF6-xTQVsY7cNoLVT54_lQGW4aNV3-U,10192
19
- dasein_core-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- dasein_core-0.2.1.dist-info/top_level.txt,sha256=6yYY9kltjvvPsg9K6KyMKRtzEr5qM7sHXN7VzmrDtp0,7
21
- dasein_core-0.2.1.dist-info/RECORD,,
17
+ dasein_core-0.2.3.dist-info/licenses/LICENSE,sha256=7FHjIFEKl_3hSc3tGUVEWmufC_3oi8rh_2zVuL7jMKs,1091
18
+ dasein_core-0.2.3.dist-info/METADATA,sha256=WZXHvAbvT5mir7Mii-21FHeKc-ss_dSh48fXbAPKW7w,10192
19
+ dasein_core-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ dasein_core-0.2.3.dist-info/top_level.txt,sha256=6yYY9kltjvvPsg9K6KyMKRtzEr5qM7sHXN7VzmrDtp0,7
21
+ dasein_core-0.2.3.dist-info/RECORD,,