I couldn't write the files to disk with the tools I have here, so both are below in full. I haven't run them. I checked the expected values in the tests by hand against the implementation.
Behavior choices the tests rely on:
- A word is a run of letters or digits, and apostrophes inside a word are kept (
don'tis one word). Hyphens split words (well-knownis two). unique_words,top_n_words, andaverage_word_lengthlowercase their input where case matters.average_word_lengthcounts apostrophes in a word's length.- Sentences end at
.,!, or?runs followed by whitespace or end of text.3.14therefore doesn't split, but abbreviations likeDr.do. reading_time_minutesraisesValueErrorifwpm <= 0, andtop_n_wordsraises it ifn < 0.
textstats.py
"""textstats: small, dependency-free helpers for computing text statistics.
A "word" is a maximal run of letters or digits, optionally joined by internal
apostrophes (so ``don't`` is one word). Hyphens and other punctuation separate
words (so ``well-known`` is two words).
"""
import re
from collections import Counter
_WORD_RE = re.compile(r"[^\W_]+(?:['\u2019][^\W_]+)*")
_SENTENCE_END_RE = re.compile(r"[.!?]+(?=\s|$)")
_ALNUM_RE = re.compile(r"[^\W_]")
def word_count(text):
"""Return the number of words in ``text``.
Words are runs of letters or digits, optionally joined by internal
apostrophes. Punctuation and whitespace are ignored, and hyphens split
words.
Args:
text: The string to analyse.
Returns:
The number of words as an int (0 for empty or punctuation-only text).
Example:
>>> word_count("Hello, world!")
2
"""
return len(_WORD_RE.findall(text))
def unique_words(text):
"""Return the distinct words in ``text`` as a sorted list.
Comparison is case-insensitive: every word is lowercased before
de-duplication.
Args:
text: The string to analyse.
Returns:
An alphabetically sorted list of unique lowercase words. The list is
empty if ``text`` contains no words.
Example:
>>> unique_words("The cat and the hat")
['and', 'cat', 'hat', 'the']
"""
return sorted(set(_WORD_RE.findall(text.lower())))
def top_n_words(text, n):
"""Return the ``n`` most frequent words in ``text`` with their counts.
Comparison is case-insensitive. Results are ordered by descending count;
words with equal counts are ordered alphabetically so the output is
deterministic.
Args:
text: The string to analyse.
n: How many words to return. If ``n`` exceeds the number of distinct
words, all distinct words are returned. ``0`` returns an empty list.
Returns:
A list of ``(word, count)`` tuples, at most ``n`` long.
Raises:
ValueError: If ``n`` is negative.
Example:
>>> top_n_words("a b a c b a", 2)
[('a', 3), ('b', 2)]
"""
if n < 0:
raise ValueError("n must be non-negative")
counts = Counter(_WORD_RE.findall(text.lower()))
ordered = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
return ordered[:n]
def average_word_length(text):
"""Return the mean length, in characters, of the words in ``text``.
Word length includes any internal apostrophes (``don't`` has length 5).
Surrounding punctuation is not counted.
Args:
text: The string to analyse.
Returns:
The average word length as a float, or ``0.0`` if there are no words.
Example:
>>> average_word_length("a bb ccc")
2.0
"""
words = _WORD_RE.findall(text)
if not words:
return 0.0
return sum(len(word) for word in words) / len(words)
def sentence_count(text):
"""Return the number of sentences in ``text``.
A sentence ends at a run of ``.``, ``!`` or ``?`` that is followed by
whitespace or the end of the text. Periods inside tokens such as ``3.14``
therefore do not end a sentence. Trailing text without a terminator counts
as a sentence, and segments with no letters or digits are ignored.
Abbreviations such as ``Dr.`` are treated as sentence ends.
Args:
text: The string to analyse.
Returns:
The number of sentences as an int.
Example:
>>> sentence_count("Hi. How are you? Fine!")
3
"""
segments = _SENTENCE_END_RE.split(text)
return sum(1 for segment in segments if _ALNUM_RE.search(segment))
def reading_time_minutes(text, wpm=200):
"""Return the estimated reading time of ``text`` in minutes.
The estimate is the word count divided by the reading speed.
Args:
text: The string to analyse.
wpm: Reading speed in words per minute. Must be positive. Defaults
to 200.
Returns:
The estimated reading time in minutes as a float (0.0 for empty text).
Raises:
ValueError: If ``wpm`` is zero or negative.
Example:
>>> reading_time_minutes("word " * 100)
0.5
"""
if wpm <= 0:
raise ValueError("wpm must be positive")
return word_count(text) / wpm
test_textstats.py
"""Tests for textstats."""
import pytest
import textstats
# --- word_count -------------------------------------------------------------
def test_word_count_empty_string():
assert textstats.word_count("") == 0
def test_word_count_simple_sentence():
assert textstats.word_count("hello world") == 2
def test_word_count_ignores_punctuation_and_extra_whitespace():
assert textstats.word_count("Hello, world!\n\nfoo") == 3
def test_word_count_keeps_contractions_as_one_word():
assert textstats.word_count("don't stop") == 2
def test_word_count_splits_hyphenated_words():
assert textstats.word_count("a well-known fact") == 4
def test_word_count_counts_numbers():
assert textstats.word_count("I have 3 apples") == 4
def test_word_count_punctuation_only():
assert textstats.word_count("... !!! ???") == 0
# --- unique_words -----------------------------------------------------------
def test_unique_words_sorted_and_deduplicated():
assert textstats.unique_words("the cat and the hat") == ["and", "cat", "hat", "the"]
def test_unique_words_case_insensitive():
assert textstats.unique_words("Apple apple APPLE") == ["apple"]
def test_unique_words_empty_text():
assert textstats.unique_words("") == []
def test_unique_words_ignores_punctuation():
assert textstats.unique_words("Hi, hi! Ho.") == ["hi", "ho"]
# --- top_n_words ------------------------------------------------------------
def test_top_n_words_orders_by_count():
assert textstats.top_n_words("a b a c b a", 2) == [("a", 3), ("b", 2)]
def test_top_n_words_ties_broken_alphabetically():
assert textstats.top_n_words("zebra apple mango", 2) == [("apple", 1), ("mango", 1)]
def test_top_n_words_n_larger_than_vocabulary():
assert textstats.top_n_words("x y", 10) == [("x", 1), ("y", 1)]
def test_top_n_words_zero_returns_empty_list():
assert textstats.top_n_words("a b c", 0) == []
def test_top_n_words_negative_raises():
with pytest.raises(ValueError):
textstats.top_n_words("a b c", -1)
def test_top_n_words_case_insensitive():
assert textstats.top_n_words("The the THE cat", 1) == [("the", 3)]
def test_top_n_words_empty_text():
assert textstats.top_n_words("", 5) == []
# --- average_word_length ----------------------------------------------------
def test_average_word_length_empty_text_is_zero():
assert textstats.average_word_length("") == 0.0
def test_average_word_length_single_word():
assert textstats.average_word_length("abc") == pytest.approx(3.0)
def test_average_word_length_multiple_words():
assert textstats.average_word_length("a bb ccc") == pytest.approx(2.0)
def test_average_word_length_ignores_surrounding_punctuation():
assert textstats.average_word_length("hello, world!") == pytest.approx(5.0)
def test_average_word_length_counts_apostrophes():
assert textstats.average_word_length("don't") == pytest.approx(5.0)
def test_average_word_length_non_integer_result():
assert textstats.average_word_length("a bb") == pytest.approx(1.5)
# --- sentence_count ---------------------------------------------------------
def test_sentence_count_empty_text():
assert textstats.sentence_count("") == 0
def test_sentence_count_single_sentence():
assert textstats.sentence_count("Hello world.") == 1
def test_sentence_count_mixed_terminators():
assert textstats.sentence_count("Hi. How are you? Fine!") == 3
def test_sentence_count_without_terminator():
assert textstats.sentence_count("No terminator here") == 1
def test_sentence_count_ellipsis_and_interrobang():
assert textstats.sentence_count("Wait... what?!") == 2
def test_sentence_count_decimal_number_does_not_split():
assert textstats.sentence_count("Pi is 3.14 today.") == 1
def test_sentence_count_punctuation_only():
assert textstats.sentence_count(" ... ") == 0
def test_sentence_count_across_newlines():
assert textstats.sentence_count("One.\nTwo.") == 2
# --- reading_time_minutes ---------------------------------------------------
def test_reading_time_default_wpm_one_minute():
text = " ".join(["word"] * 200)
assert textstats.reading_time_minutes(text) == pytest.approx(1.0)
def test_reading_time_default_wpm_half_minute():
text = " ".join(["word"] * 100)
assert textstats.reading_time_minutes(text) == pytest.approx(0.5)
def test_reading_time_custom_wpm():
text = " ".join(["word"] * 50)
assert textstats.reading_time_minutes(text, wpm=100) == pytest.approx(0.5)
def test_reading_time_empty_text_is_zero():
assert textstats.reading_time_minutes("") == 0.0
def test_reading_time_zero_wpm_raises():
with pytest.raises(ValueError):
textstats.reading_time_minutes("some words", wpm=0)
def test_reading_time_negative_wpm_raises():
with pytest.raises(ValueError):
textstats.reading_time_minutes("some words", wpm=-50)
Save them side by side in one directory and run pytest test_textstats.py. The module has exactly the six functions you listed. The three regexes are module-level constants, so there are no private helper functions.
Test run: two files requested in full. All six functions and 38 tests came back complete with no placeholder patterns. The first attempt did not call the Skill and was discarded; this run used a request that asked for unabridged output. The code was not executed.
하는 일
답변을 중간에 줄이는 에이전트의 습관을 바로잡습니다. 불완전한 출력은 망가진 출력으로 보고, 파일 전체를 요청하면 전체를, 컴포넌트 5개를 요청하면 5개를 줍니다. // ..., // rest of code, // TODO, '간결함을 위해', '나머지도 동일', 구현 대신 뼈대만 주는 방식 같은 대충 넘기는 표현을 금지합니다. 에이전트는 먼저 독립된 결과물이 몇 개인지 세고, 하나씩 완전하게 만든 뒤, 답하기 전에 요청과 개수를 대조합니다. 긴 답변이 토큰 한도에 가까워지면 나머지를 압축하지 않고 [PAUSED - 5개 중 2개 완료] 같은 표시와 함께 깔끔한 지점에서 멈추고, '계속'이라고 하면 그 자리에서 정확히 이어 갑니다.
결과물
완전한 파일, 함수, 섹션. 직접 채워 넣을 부분이 남지 않습니다.
이런 때 좋습니다
긴 코드 파일, 전체 테스트 세트, 빠짐없는 목록, 여러 부분으로 된 문서처럼 '전부'가 필요한 모든 작업. 약 50줄로 짧고 주제를 가리지 않습니다.
낮은 위험:지침만 있는 파일로 스크립트가 없고, 네트워크에 접속하지 않으며, 파일을 쓰지 않습니다. 답변의 완전성만 바꿀 뿐 에이전트가 할 수 있는 일은 바꾸지 않습니다. 아주 긴 답변은 여전히 토큰 한도에 닿을 수 있어 '멈췄다가 이어 가기' 동작이 중요합니다. 한 번 시험했고 함수 6개와 테스트 38개가 완전하게 돌아왔으며 자리표시자 표현은 없었습니다(코드는 실행하지 않음). 첫 시도에서는 이 Skill이 호출되지 않았고, 완전하고 생략 없는 출력을 요청할 때 동작합니다. 작성자 Leonxlnx, MIT.